From c73d5f6967de5362af7d6f75c6c593a6e300efa3 Mon Sep 17 00:00:00 2001 From: Alexander Alexandrov Date: Fri, 13 Jan 2023 14:05:32 +0200 Subject: [PATCH 1/6] sql: remove `qgm_optimizations` from `OptimizerConfig` --- src/adapter/src/coord/sequencer.rs | 8 ++--- src/sql/src/plan/optimize.rs | 52 +++--------------------------- 2 files changed, 7 insertions(+), 53 deletions(-) diff --git a/src/adapter/src/coord/sequencer.rs b/src/adapter/src/coord/sequencer.rs index c748a39786ae..ba7296c8e080 100644 --- a/src/adapter/src/coord/sequencer.rs +++ b/src/adapter/src/coord/sequencer.rs @@ -2691,9 +2691,7 @@ impl Coordinator { }); // run optimization pipeline - let decorrelated_plan = raw_plan.optimize_and_lower(&OptimizerConfig { - qgm_optimizations: session.vars().qgm_optimizations(), - })?; + let decorrelated_plan = raw_plan.optimize_and_lower(&OptimizerConfig {})?; self.validate_timeline_context(decorrelated_plan.depends_on())?; @@ -2904,9 +2902,7 @@ impl Coordinator { raw_plan, format, .. } = plan; - let decorrelated_plan = raw_plan.optimize_and_lower(&OptimizerConfig { - qgm_optimizations: session.vars().qgm_optimizations(), - })?; + let decorrelated_plan = raw_plan.optimize_and_lower(&OptimizerConfig {})?; let optimized_plan = self.view_optimizer.optimize(decorrelated_plan)?; let source_ids = optimized_plan.depends_on(); let compute_instance = self.catalog.active_compute_instance(session)?; diff --git a/src/sql/src/plan/optimize.rs b/src/sql/src/plan/optimize.rs index 89e8fb0ddf8e..725b0bb1f581 100644 --- a/src/sql/src/plan/optimize.rs +++ b/src/sql/src/plan/optimize.rs @@ -9,30 +9,19 @@ ///! This module defines the API and logic for running optimization pipelines. use crate::plan::expr::HirRelationExpr; -use crate::query_model::{Model, QGMError}; use super::{PlanError, StatementContext}; /// Feature flags for the [`HirRelationExpr::optimize_and_lower()`] logic. #[derive(Debug)] pub struct OptimizerConfig { - pub qgm_optimizations: bool, + // TODO: add parameters driving the optimization pass here } /// Convert a reference to a [`StatementContext`] to an [`OptimizerConfig`]. -/// -/// This picks up feature flag values such as `qgm_optimizations` from the `PlanContext` if this is present in -/// the [`StatementContext`], otherwise uses sensible defaults. impl<'a> From<&StatementContext<'a>> for OptimizerConfig { - fn from(scx: &StatementContext) -> Self { - match scx.pcx() { - Ok(pcx) => OptimizerConfig { - qgm_optimizations: pcx.qgm_optimizations, - }, - Err(..) => OptimizerConfig { - qgm_optimizations: false, - }, - } + fn from(_scx: &StatementContext) -> Self { + OptimizerConfig {} } } @@ -42,39 +31,8 @@ impl HirRelationExpr { /// The optimization path is fully-determined by the values of the feature flag defined in the [`OptimizerConfig`]. pub fn optimize_and_lower( self, - config: &OptimizerConfig, + _config: &OptimizerConfig, ) -> Result { - if config.qgm_optimizations { - // try to go through the QGM path - self.try_qgm_path().map_err(Into::into) - } else { - // directly decorrelate and lower into a MirRelationExpr - Ok(self.lower()) - } - } - - /// Attempt an optimization path from HIR to MIR that goes through a QGM representation. - /// - /// Return `Result::Err` if the path is not possible. - #[tracing::instrument(target = "optimizer", level = "debug", name = "qgm", skip_all)] - fn try_qgm_path(self) -> Result { - // create a query graph model from this HirRelationExpr - let mut model = Model::try_from(self)?; - - tracing::span!(tracing::Level::INFO, "raw").in_scope(|| { - // TODO: this requires to implement Clone for Model - // mz_repr::explain_new::trace_plan(model); - }); - - // perform optimizing algebraic rewrites on the qgm - model.optimize(); - - tracing::span!(tracing::Level::INFO, "raw").in_scope(|| { - // TODO: this requires to implement Clone for Model - // mz_repr::explain_new::trace_plan(model); - }); - - // decorrelate and lower the optimized query graph model into a MirRelationExpr - model.try_into() + Ok(self.lower()) } } From f285d2d5fdef00a9ca98a653b27717362c76f6c3 Mon Sep 17 00:00:00 2001 From: Alexander Alexandrov Date: Fri, 13 Jan 2023 14:44:01 +0200 Subject: [PATCH 2/6] sql: remove `qgm_optimizations` from PlanContext --- src/adapter/src/session.rs | 4 ++-- src/sql/src/func.rs | 2 +- src/sql/src/plan.rs | 9 ++------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/adapter/src/session.rs b/src/adapter/src/session.rs index 1ec81c43f5d6..1dfbbd8caa89 100644 --- a/src/adapter/src/session.rs +++ b/src/adapter/src/session.rs @@ -190,7 +190,7 @@ impl Session { let id = self.next_transaction_id; self.next_transaction_id = self.next_transaction_id.wrapping_add(1); self.transaction = TransactionStatus::InTransaction(Transaction { - pcx: PlanContext::new(wall_time, self.vars.qgm_optimizations()), + pcx: PlanContext::new(wall_time), ops: TransactionOps::None, write_lock_guard: None, access, @@ -224,7 +224,7 @@ impl Session { let id = self.next_transaction_id; self.next_transaction_id = self.next_transaction_id.wrapping_add(1); let txn = Transaction { - pcx: PlanContext::new(wall_time, self.vars.qgm_optimizations()), + pcx: PlanContext::new(wall_time), ops: TransactionOps::None, write_lock_guard: None, access: None, diff --git a/src/sql/src/func.rs b/src/sql/src/func.rs index cbff44da77f0..7b671f86fb70 100644 --- a/src/sql/src/func.rs +++ b/src/sql/src/func.rs @@ -1709,7 +1709,7 @@ macro_rules! impl_def { // Return type can be automatically determined as a function of the // parameters. ($params:expr, $op:expr, $oid:expr) => {{ - let pcx = crate::plan::PlanContext::new(DateTime::::MIN_UTC, false); + let pcx = crate::plan::PlanContext::new(DateTime::::MIN_UTC); let scx = StatementContext::new(None, &crate::catalog::DummyCatalog); // This lifetime is compatible with more functions. let qcx = QueryContext::root(&scx, QueryLifetime::OneShot(&pcx)); diff --git a/src/sql/src/plan.rs b/src/sql/src/plan.rs index 23ecdc1ba1f8..fba5aee54693 100644 --- a/src/sql/src/plan.rs +++ b/src/sql/src/plan.rs @@ -835,15 +835,11 @@ impl Params { #[derive(Ord, PartialOrd, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash, Copy)] pub struct PlanContext { pub wall_time: DateTime, - pub qgm_optimizations: bool, } impl PlanContext { - pub fn new(wall_time: DateTime, qgm_optimizations: bool) -> Self { - Self { - wall_time, - qgm_optimizations, - } + pub fn new(wall_time: DateTime) -> Self { + Self { wall_time } } /// Return a PlanContext with zero values. This should only be used when @@ -852,7 +848,6 @@ impl PlanContext { pub fn zero() -> Self { PlanContext { wall_time: now::to_datetime(NOW_ZERO()), - qgm_optimizations: false, } } } From 047ab2e6f56c6fcfa94238e12b4a13600519ee3a Mon Sep 17 00:00:00 2001 From: Alexander Alexandrov Date: Fri, 13 Jan 2023 14:00:03 +0200 Subject: [PATCH 3/6] adapter: remove `qgm_optimizations` session variable --- src/adapter/src/session/vars.rs | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/adapter/src/session/vars.rs b/src/adapter/src/session/vars.rs index 53a2c30a375c..0f5cba06873f 100644 --- a/src/adapter/src/session/vars.rs +++ b/src/adapter/src/session/vars.rs @@ -129,13 +129,6 @@ const INTERVAL_STYLE: ServerVar = ServerVar { const MZ_VERSION_NAME: &UncasedStr = UncasedStr::new("mz_version"); -const QGM_OPTIMIZATIONS: ServerVar = ServerVar { - name: UncasedStr::new("qgm_optimizations_experimental"), - value: &false, - description: "Enables optimizations based on a Query Graph Model (QGM) query representation.", - internal: false, -}; - static DEFAULT_SEARCH_PATH: Lazy<[String; 1]> = Lazy::new(|| [DEFAULT_SCHEMA.to_owned()]); static SEARCH_PATH: Lazy> = Lazy::new(|| ServerVar { name: UncasedStr::new("search_path"), @@ -424,7 +417,6 @@ pub struct SessionVars { failpoints: ServerVar, integer_datetimes: ServerVar, interval_style: ServerVar, - qgm_optimizations: SessionVar, search_path: SessionVar<[String]>, server_version: ServerVar, server_version_num: ServerVar, @@ -455,7 +447,6 @@ impl SessionVars { failpoints: FAILPOINTS, integer_datetimes: INTEGER_DATETIMES, interval_style: INTERVAL_STYLE, - qgm_optimizations: SessionVar::new(&QGM_OPTIMIZATIONS), search_path: SessionVar::new(&SEARCH_PATH), server_version: SERVER_VERSION, server_version_num: SERVER_VERSION_NUM, @@ -483,7 +474,7 @@ impl SessionVars { /// Returns an iterator over the configuration parameters and their current /// values for this session. pub fn iter(&self) -> impl Iterator { - let vars: [&dyn Var; 25] = [ + let vars: [&dyn Var; 24] = [ &self.application_name, self.build_info, &self.client_encoding, @@ -496,7 +487,6 @@ impl SessionVars { &self.failpoints, &self.integer_datetimes, &self.interval_style, - &self.qgm_optimizations, &self.search_path, &self.server_version, &self.server_version_num, @@ -572,8 +562,6 @@ impl SessionVars { Ok(&self.interval_style) } else if name == MZ_VERSION_NAME { Ok(self.build_info) - } else if name == QGM_OPTIMIZATIONS.name { - Ok(&self.qgm_optimizations) } else if name == SEARCH_PATH.name { Ok(&self.search_path) } else if name == SERVER_VERSION.name { @@ -690,8 +678,6 @@ impl SessionVars { } else { Ok(()) } - } else if name == QGM_OPTIMIZATIONS.name { - self.qgm_optimizations.set(value, local) } else if name == SEARCH_PATH.name { self.search_path.set(value, local) } else if name == SERVER_VERSION.name { @@ -768,8 +754,6 @@ impl SessionVars { self.database.reset(local); } else if name == EXTRA_FLOAT_DIGITS.name { self.extra_float_digits.reset(local); - } else if name == QGM_OPTIMIZATIONS.name { - self.qgm_optimizations.reset(local); } else if name == SEARCH_PATH.name { self.search_path.reset(local); } else if name == SQL_SAFE_UPDATES.name { @@ -822,7 +806,6 @@ impl SessionVars { failpoints: _, integer_datetimes: _, interval_style: _, - qgm_optimizations, search_path, server_version: _, server_version_num: _, @@ -842,7 +825,6 @@ impl SessionVars { cluster_replica.end_transaction(action); database.end_transaction(action); extra_float_digits.end_transaction(action); - qgm_optimizations.end_transaction(action); search_path.end_transaction(action); sql_safe_updates.end_transaction(action); statement_timeout.end_transaction(action); @@ -914,11 +896,6 @@ impl SessionVars { self.build_info.value() } - /// Returns the value of the `qgm_optimizations` configuration parameter. - pub fn qgm_optimizations(&self) -> bool { - *self.qgm_optimizations.value() - } - /// Returns the value of the `search_path` configuration parameter. pub fn search_path(&self) -> Vec<&str> { self.search_path From 8d6ad365d4b64cecd3205e6fc840a0004b1267d5 Mon Sep 17 00:00:00 2001 From: Alexander Alexandrov Date: Fri, 13 Jan 2023 14:46:56 +0200 Subject: [PATCH 4/6] sql: remove `ExplainStage::(Optimized)QueryGraph` --- src/adapter/src/coord/sequencer.rs | 65 +++--------------------- src/sql-parser/src/ast/defs/statement.rs | 8 --- src/sql-parser/src/parser.rs | 13 +---- src/sql/src/plan/statement/dml.rs | 8 --- 4 files changed, 10 insertions(+), 84 deletions(-) diff --git a/src/adapter/src/coord/sequencer.rs b/src/adapter/src/coord/sequencer.rs index ba7296c8e080..5743a8025af0 100644 --- a/src/adapter/src/coord/sequencer.rs +++ b/src/adapter/src/coord/sequencer.rs @@ -2677,13 +2677,11 @@ impl Coordinator { let optimizer_trace = match stage { Trace => OptimizerTrace::new(), // collect all trace entries - QueryGraph | OptimizedQueryGraph => OptimizerTrace::find(""), // don't collect anything stage => OptimizerTrace::find(stage.path()), // collect a trace entry only the selected stage }; let (used_indexes, fast_path_plan) = optimizer_trace.collect_trace(|| -> Result<_, AdapterError> { - let raw_plan = raw_plan.clone(); // FIXME: remove `.clone()` once the QGM Model implements Clone. let _span = tracing::span!(Level::INFO, "optimize").entered(); tracing::span!(Level::INFO, "raw").in_scope(|| { @@ -2737,63 +2735,16 @@ impl Coordinator { Ok((used_indexes, fast_path_plan)) })?; - let trace = if matches!(stage, QueryGraph | OptimizedQueryGraph) { - vec![] // FIXME: remove this case once the QGM Model implements Clone - } else { - optimizer_trace.drain_all( - format.clone(), // FIXME: remove `.clone()` once the QGM Model implements Clone - config.clone(), // FIXME: remove `.clone()` once the QGM Model implements Clone - self.catalog.for_session(session), - row_set_finishing.clone(), // FIXME: remove `.clone()` once the QGM Model implements Clone - used_indexes, - fast_path_plan, - )? - }; + let trace = optimizer_trace.drain_all( + format, + config, + self.catalog.for_session(session), + row_set_finishing, + used_indexes, + fast_path_plan, + )?; let rows = match stage { - // QGM graphs are not collected in the trace at the moment as they - // do not implement Clone (see the TODOs in try_qgm_path. - // Once this is done the next two cases will be handled by the catch-all - // case at the end of this method. - QueryGraph => { - use mz_repr::explain_new::Explain; - // run partial pipeline - let mut model = mz_sql::query_model::Model::try_from(raw_plan)?; - // construct explanation context - let catalog = self.catalog.for_session(session); - let context = crate::explain_new::ExplainContext { - config: &config, - humanizer: &catalog, - used_indexes: crate::explain_new::UsedIndexes::new(Default::default()), - finishing: row_set_finishing, - fast_path_plan: Default::default(), - }; - // explain plan - let mut explainable = crate::explain_new::Explainable::new(&mut model); - let explanation_string = explainable.explain(&format, &config, &context)?; - // pack rows in result vector - vec![Row::pack_slice(&[Datum::from(&*explanation_string)])] - } - OptimizedQueryGraph => { - use mz_repr::explain_new::Explain; - // run partial pipeline - let mut model = mz_sql::query_model::Model::try_from(raw_plan)?; - model.optimize(); - // construct explanation context - let catalog = self.catalog.for_session(session); - let context = crate::explain_new::ExplainContext { - config: &config, - humanizer: &catalog, - used_indexes: crate::explain_new::UsedIndexes::new(Default::default()), - finishing: row_set_finishing, - fast_path_plan: Default::default(), - }; - // explain plan - let mut explainable = crate::explain_new::Explainable::new(&mut model); - let explanation_string = explainable.explain(&format, &config, &context)?; - // pack rows in result vector - vec![Row::pack_slice(&[Datum::from(&*explanation_string)])] - } // For the `Trace` stage, return the entire trace as (time, path, plan) triples. Trace => { let rows = trace diff --git a/src/sql-parser/src/ast/defs/statement.rs b/src/sql-parser/src/ast/defs/statement.rs index 0440cf610619..a2e3362cf2f1 100644 --- a/src/sql-parser/src/ast/defs/statement.rs +++ b/src/sql-parser/src/ast/defs/statement.rs @@ -2387,10 +2387,6 @@ impl_display_t!(Assignment); pub enum ExplainStage { /// The mz_sql::HirRelationExpr after parsing RawPlan, - /// Query Graph - QueryGraph, - /// Optimized Query Graph - OptimizedQueryGraph, /// The mz_expr::MirRelationExpr after decorrelation DecorrelatedPlan, /// The mz_expr::MirRelationExpr after optimization @@ -2408,8 +2404,6 @@ impl ExplainStage { pub fn path(&self) -> &'static str { match self { ExplainStage::RawPlan => "optimize/raw", - ExplainStage::QueryGraph => "optimize/qgm/raw", - ExplainStage::OptimizedQueryGraph => "optimize/qgm/optimized", ExplainStage::DecorrelatedPlan => "optimize/hir_to_mir", ExplainStage::OptimizedPlan => "optimize/global", ExplainStage::PhysicalPlan => "optimize/mir_to_lir", @@ -2423,8 +2417,6 @@ impl AstDisplay for ExplainStage { fn fmt(&self, f: &mut AstFormatter) { match self { ExplainStage::RawPlan => f.write_str("RAW PLAN"), - ExplainStage::OptimizedQueryGraph => f.write_str("OPTIMIZED QUERY GRAPH"), - ExplainStage::QueryGraph => f.write_str("QUERY GRAPH"), ExplainStage::DecorrelatedPlan => f.write_str("DECORRELATED PLAN"), ExplainStage::OptimizedPlan => f.write_str("OPTIMIZED PLAN"), ExplainStage::PhysicalPlan => f.write_str("PHYSICAL PLAN"), diff --git a/src/sql-parser/src/parser.rs b/src/sql-parser/src/parser.rs index 7740c0b57087..13768be228cf 100644 --- a/src/sql-parser/src/parser.rs +++ b/src/sql-parser/src/parser.rs @@ -5545,22 +5545,13 @@ impl<'a> Parser<'a> { self.expect_keyword(PLAN)?; Some(ExplainStage::RawPlan) } - Some(QUERY) => { - self.expect_keyword(GRAPH)?; - Some(ExplainStage::QueryGraph) - } Some(DECORRELATED) => { self.expect_keyword(PLAN)?; Some(ExplainStage::DecorrelatedPlan) } Some(OPTIMIZED) => { - if self.parse_keyword(QUERY) { - self.expect_keyword(GRAPH)?; - Some(ExplainStage::OptimizedQueryGraph) - } else { - self.expect_keyword(PLAN)?; - Some(ExplainStage::OptimizedPlan) - } + self.expect_keyword(PLAN)?; + Some(ExplainStage::OptimizedPlan) } Some(PLAN) => Some(ExplainStage::OptimizedPlan), // EXPLAIN PLAN ~= EXPLAIN OPTIMIZED PLAN Some(PHYSICAL) => { diff --git a/src/sql/src/plan/statement/dml.rs b/src/sql/src/plan/statement/dml.rs index e51e89e4b568..b463e061bed6 100644 --- a/src/sql/src/plan/statement/dml.rs +++ b/src/sql/src/plan/statement/dml.rs @@ -195,14 +195,6 @@ pub fn describe_explain( relation_desc = relation_desc.with_column("Raw Plan", ScalarType::String.nullable(false)); } - ExplainStage::QueryGraph => { - relation_desc = - relation_desc.with_column("Query Graph", ScalarType::String.nullable(false)); - } - ExplainStage::OptimizedQueryGraph => { - relation_desc = relation_desc - .with_column("Optimized Query Graph", ScalarType::String.nullable(false)); - } ExplainStage::DecorrelatedPlan => { relation_desc = relation_desc.with_column("Decorrelated Plan", ScalarType::String.nullable(false)); From 6852884d0fdb41d5b8847a70537730f39039b2a7 Mon Sep 17 00:00:00 2001 From: Alexander Alexandrov Date: Fri, 13 Jan 2023 14:54:29 +0200 Subject: [PATCH 5/6] sql: remove QGM code and tests --- ci/test/slt-fast.sh | 1 - src/adapter/src/error.rs | 10 - src/adapter/src/explain_new/mod.rs | 1 - src/adapter/src/explain_new/qgm/dot.rs | 46 - src/adapter/src/explain_new/qgm/mod.rs | 37 - src/pgwire/src/message.rs | 1 - src/sql-parser/src/keywords.txt | 1 - src/sql-parser/tests/testdata/explain | 7 - src/sql/src/lib.rs | 1 - src/sql/src/plan/error.rs | 9 - src/sql/src/query_model/README.md | 38 - src/sql/src/query_model/attribute/core.rs | 347 --- src/sql/src/query_model/attribute/mod.rs | 15 - .../query_model/attribute/propagated_nulls.rs | 161 -- .../query_model/attribute/rejected_nulls.rs | 554 ----- .../query_model/attribute/relation_type.rs | 516 ----- src/sql/src/query_model/dot.rs | 362 --- src/sql/src/query_model/error.rs | 222 -- src/sql/src/query_model/hir/hir_from_qgm.rs | 418 ---- src/sql/src/query_model/hir/mod.rs | 17 - src/sql/src/query_model/hir/qgm_from_hir.rs | 553 ----- src/sql/src/query_model/mir.rs | 783 ------- src/sql/src/query_model/mod.rs | 23 - src/sql/src/query_model/model/graph.rs | 996 -------- src/sql/src/query_model/model/mod.rs | 14 - src/sql/src/query_model/model/scalar.rs | 266 --- src/sql/src/query_model/rewrite/mod.rs | 343 --- src/sql/src/query_model/rewrite/rule/mod.rs | 18 - .../rewrite/rule/simplify_outer_joins.rs | 223 -- src/sql/src/query_model/test/catalog.rs | 379 ---- src/sql/src/query_model/test/mod.rs | 146 -- src/sql/src/query_model/test/util.rs | 182 -- src/sql/src/query_model/validator/mod.rs | 94 - .../src/query_model/validator/quantifier.rs | 560 ----- .../explain/optimized_query_graph_as_dot.slt | 115 - .../explain/query_graph_as_dot.slt | 139 -- test/sqllogictest/qgm/qgm-type-inference.slt | 2021 ----------------- test/sqllogictest/query_graph.slt | 45 - 38 files changed, 9664 deletions(-) delete mode 100644 src/adapter/src/explain_new/qgm/dot.rs delete mode 100644 src/adapter/src/explain_new/qgm/mod.rs delete mode 100644 src/sql/src/query_model/README.md delete mode 100644 src/sql/src/query_model/attribute/core.rs delete mode 100644 src/sql/src/query_model/attribute/mod.rs delete mode 100644 src/sql/src/query_model/attribute/propagated_nulls.rs delete mode 100644 src/sql/src/query_model/attribute/rejected_nulls.rs delete mode 100644 src/sql/src/query_model/attribute/relation_type.rs delete mode 100644 src/sql/src/query_model/dot.rs delete mode 100644 src/sql/src/query_model/error.rs delete mode 100644 src/sql/src/query_model/hir/hir_from_qgm.rs delete mode 100644 src/sql/src/query_model/hir/mod.rs delete mode 100644 src/sql/src/query_model/hir/qgm_from_hir.rs delete mode 100644 src/sql/src/query_model/mir.rs delete mode 100644 src/sql/src/query_model/mod.rs delete mode 100644 src/sql/src/query_model/model/graph.rs delete mode 100644 src/sql/src/query_model/model/mod.rs delete mode 100644 src/sql/src/query_model/model/scalar.rs delete mode 100644 src/sql/src/query_model/rewrite/mod.rs delete mode 100644 src/sql/src/query_model/rewrite/rule/mod.rs delete mode 100644 src/sql/src/query_model/rewrite/rule/simplify_outer_joins.rs delete mode 100644 src/sql/src/query_model/test/catalog.rs delete mode 100644 src/sql/src/query_model/test/mod.rs delete mode 100644 src/sql/src/query_model/test/util.rs delete mode 100644 src/sql/src/query_model/validator/mod.rs delete mode 100644 src/sql/src/query_model/validator/quantifier.rs delete mode 100644 test/sqllogictest/explain/optimized_query_graph_as_dot.slt delete mode 100644 test/sqllogictest/explain/query_graph_as_dot.slt delete mode 100644 test/sqllogictest/qgm/qgm-type-inference.slt delete mode 100644 test/sqllogictest/query_graph.slt diff --git a/ci/test/slt-fast.sh b/ci/test/slt-fast.sh index 01741e47f858..45c4425f9529 100755 --- a/ci/test/slt-fast.sh +++ b/ci/test/slt-fast.sh @@ -17,7 +17,6 @@ tests=( test/sqllogictest/*.slt test/sqllogictest/attributes/*.slt \ test/sqllogictest/explain/*.slt - test/sqllogictest/qgm/*.slt test/sqllogictest/autogenerated/*.slt test/sqllogictest/transform/*.slt # test/sqllogictest/sqlite/test/evidence/in1.test diff --git a/src/adapter/src/error.rs b/src/adapter/src/error.rs index 9216eebe5092..8144c67ed9f9 100644 --- a/src/adapter/src/error.rs +++ b/src/adapter/src/error.rs @@ -22,7 +22,6 @@ use mz_ore::str::StrExt; use mz_repr::explain_new::ExplainError; use mz_repr::NotNullViolation; use mz_sql::plan::PlanError; -use mz_sql::query_model::QGMError; use mz_storage_client::controller::StorageError; use mz_transform::TransformError; @@ -109,8 +108,6 @@ pub enum AdapterError { PlanError(PlanError), /// The named prepared statement already exists. PreparedStatementExists(String), - /// An error occurred in the QGM stage of the optimizer. - QGM(QGMError), /// The transaction is in read-only mode. ReadOnlyTransaction, /// The specified session parameter is read-only. @@ -401,7 +398,6 @@ impl fmt::Display for AdapterError { AdapterError::PreparedStatementExists(name) => { write!(f, "prepared statement {} already exists", name.quoted()) } - AdapterError::QGM(e) => e.fmt(f), AdapterError::ReadOnlyTransaction => f.write_str("transaction in read-only mode"), AdapterError::ReadOnlyParameter(p) => { write!(f, "parameter {} cannot be changed", p.name().quoted()) @@ -552,12 +548,6 @@ impl From for AdapterError { } } -impl From for AdapterError { - fn from(e: QGMError) -> AdapterError { - AdapterError::QGM(e) - } -} - impl From for AdapterError { fn from(e: TransformError) -> AdapterError { AdapterError::Transform(e) diff --git a/src/adapter/src/explain_new/mod.rs b/src/adapter/src/explain_new/mod.rs index 12378a10b5e8..5f7bab1700d4 100644 --- a/src/adapter/src/explain_new/mod.rs +++ b/src/adapter/src/explain_new/mod.rs @@ -33,7 +33,6 @@ pub(crate) mod hir; pub(crate) mod lir; pub(crate) mod mir; pub(crate) mod optimizer_trace; -pub(crate) mod qgm; /// Newtype struct for wrapping types that should /// implement the [`mz_repr::explain_new::Explain`] trait. diff --git a/src/adapter/src/explain_new/qgm/dot.rs b/src/adapter/src/explain_new/qgm/dot.rs deleted file mode 100644 index 878a94e64113..000000000000 --- a/src/adapter/src/explain_new/qgm/dot.rs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -//! `EXPLAIN ... AS DOT` support for QGM structures. - -use std::fmt; - -use mz_repr::explain_new::DisplayDot; - -/// A very naive way of representing a -/// [`mz_repr::explain_new::ExplainFormat::Dot`] explanation for an -/// [`mz_sql::query_model::Model`]. -/// -/// Because `mz_sql::query_model::dot::DotGenerator::generate` is -/// mutable, we cannot use `DotGenerator` as an DOT explanation type -/// for QGM graphs and call `generate` from within its [`DisplayDot`] -/// implementation. -/// -/// To accomodate for the current code structure, we therefore generate -/// the DOT output in the [`mz_repr::explain_new::Explain::explain_dot`] -/// call for the [`mz_sql::query_model::Model`] implementation instead -/// and wrap the resulting string into this naive explanation type. -/// -/// In the long term: -/// 1. `DotGenerator` should be moved to this package, -/// 2. The generate method should be moved to `DotFormat::fmt_dot, -/// 3. Attribute derivation should happen in the `explain_dot` method. -pub struct ModelDotExplanation(String); - -impl From for ModelDotExplanation { - fn from(string: String) -> Self { - ModelDotExplanation(string) - } -} - -impl DisplayDot<()> for ModelDotExplanation { - fn fmt_dot(&self, f: &mut fmt::Formatter<'_>, _ctx: &mut ()) -> fmt::Result { - f.write_str(&self.0) - } -} diff --git a/src/adapter/src/explain_new/qgm/mod.rs b/src/adapter/src/explain_new/qgm/mod.rs deleted file mode 100644 index 7c4391599784..000000000000 --- a/src/adapter/src/explain_new/qgm/mod.rs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -//! `EXPLAIN` support for `QGM` structures. - -pub(crate) mod dot; - -use mz_repr::explain_new::{Explain, ExplainConfig, ExplainError, UnsupportedFormat}; -use mz_sql::query_model::Model; - -use super::{ExplainContext, Explainable}; -use dot::ModelDotExplanation; - -impl<'a> Explain<'a> for Explainable<'a, Model> { - type Context = ExplainContext<'a>; - - type Text = UnsupportedFormat; - - type Json = UnsupportedFormat; - - type Dot = ModelDotExplanation; // FIXME: see ModelDotExplanation docs - - fn explain_dot( - &'a mut self, - config: &'a ExplainConfig, - context: &'a Self::Context, - ) -> Result { - let result = self.0.as_dot("", context.humanizer, config.types)?; - Ok(ModelDotExplanation::from(result)) - } -} diff --git a/src/pgwire/src/message.rs b/src/pgwire/src/message.rs index e7e944fabbf1..abe77d36ccd3 100644 --- a/src/pgwire/src/message.rs +++ b/src/pgwire/src/message.rs @@ -347,7 +347,6 @@ impl ErrorResponse { AdapterError::OperationRequiresTransaction(_) => SqlState::NO_ACTIVE_SQL_TRANSACTION, AdapterError::PlanError(_) => SqlState::INTERNAL_ERROR, AdapterError::PreparedStatementExists(_) => SqlState::DUPLICATE_PSTATEMENT, - AdapterError::QGM(_) => SqlState::INTERNAL_ERROR, AdapterError::ReadOnlyTransaction => SqlState::READ_ONLY_SQL_TRANSACTION, AdapterError::ReadOnlyParameter(_) => SqlState::CANT_CHANGE_RUNTIME_PARAM, AdapterError::ReadWriteUnavailable => SqlState::INVALID_TRANSACTION_STATE, diff --git a/src/sql-parser/src/keywords.txt b/src/sql-parser/src/keywords.txt index df15fb9cb85e..9041f0dac2a1 100644 --- a/src/sql-parser/src/keywords.txt +++ b/src/sql-parser/src/keywords.txt @@ -140,7 +140,6 @@ From Full Fullname Generator -Graph Greatest Group Groups diff --git a/src/sql-parser/tests/testdata/explain b/src/sql-parser/tests/testdata/explain index 145fdb35c10c..5531e2ce577c 100644 --- a/src/sql-parser/tests/testdata/explain +++ b/src/sql-parser/tests/testdata/explain @@ -103,13 +103,6 @@ EXPLAIN TIMESTAMP AS TEXT FOR SELECT 1 => Explain(ExplainStatement { stage: Timestamp, config_flags: [], format: Text, explainee: Query(Query { ctes: Simple([]), body: Select(Select { distinct: None, projection: [Expr { expr: Value(Number("1")), alias: None }], from: [], selection: None, group_by: [], having: None, options: [] }), order_by: [], limit: None, offset: None }) }) -parse-statement -EXPLAIN OPTIMIZED QUERY GRAPH WITH (monotonicity, types) AS TEXT FOR VIEW foo ----- -EXPLAIN OPTIMIZED QUERY GRAPH WITH(monotonicity, types) AS TEXT FOR VIEW foo -=> -Explain(ExplainStatement { stage: OptimizedQueryGraph, config_flags: [Ident("monotonicity"), Ident("types")], format: Text, explainee: View(Name(UnresolvedObjectName([Ident("foo")]))) }) - parse-statement EXPLAIN AS JSON SELECT * FROM foo ---- diff --git a/src/sql/src/lib.rs b/src/sql/src/lib.rs index f2618e95e190..97f993626499 100644 --- a/src/sql/src/lib.rs +++ b/src/sql/src/lib.rs @@ -173,4 +173,3 @@ pub mod normalize; pub mod parse; pub mod plan; pub mod pure; -pub mod query_model; diff --git a/src/sql/src/plan/error.rs b/src/sql/src/plan/error.rs index 0a8f94117b28..3e38bba3c493 100644 --- a/src/sql/src/plan/error.rs +++ b/src/sql/src/plan/error.rs @@ -34,7 +34,6 @@ use crate::names::PartialObjectName; use crate::names::ResolvedObjectName; use crate::plan::plan_utils::JoinSide; use crate::plan::scope::ScopeItem; -use crate::query_model::QGMError; #[derive(Clone, Debug)] pub enum PlanError { @@ -83,7 +82,6 @@ pub enum PlanError { InvalidSecret(ResolvedObjectName), InvalidTemporarySchema, Parser(ParserError), - Qgm(QGMError), DropViewOnMaterializedView(String), DropSubsource { subsource: String, @@ -319,7 +317,6 @@ impl fmt::Display for PlanError { Self::Unstructured(e) => write!(f, "{}", e), Self::InvalidObject(i) => write!(f, "{} is not a database object", i.full_name_str()), Self::InvalidSecret(i) => write!(f, "{} is not a secret", i.full_name_str()), - Self::Qgm(e) => e.fmt(f), Self::InvalidTemporarySchema => { write!(f, "cannot create temporary item in non-temporary schema") } @@ -455,12 +452,6 @@ impl From for PlanError { } } -impl From for PlanError { - fn from(e: QGMError) -> PlanError { - PlanError::Qgm(e) - } -} - struct ColumnDisplay<'a> { table: &'a Option, column: &'a ColumnName, diff --git a/src/sql/src/query_model/README.md b/src/sql/src/query_model/README.md deleted file mode 100644 index 22f37335c5fe..000000000000 --- a/src/sql/src/query_model/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Query Graph Model - -This module contains the implementation of the Query Graph Model as specified -[here](../../../../doc/developer/design/20210707_qgm_sql_high_level_representation.md). - -## Testing - -Tests around the Query Graph Model, for both the model generation logic and the -model transformat logic, are usually `datadriven` tests that result in multiple -`graphviz` graphs, that need to be rendered and visually validated. - -### Linux - -The following shell function extracts all the `graphviz` graphs containing in -one of these tests and renders them as PNG files. The snippet after it shows -how it can be used with one of these tests. - -```sh -extract_graph() { - sed -n "/^digraph G {$/,/^}$/p" $1 | csplit --prefix $1- --suffix-format "%04d.dot" --elide-empty-files - '/^digraph G {$/' '{*}' - for i in $1-*.dot; do - dot -Tpng -O $i - done -} -``` - -``` -$ extract_graph src/sql/tests/querymodel/basic -... -$ eog src/sql/tests/querymodel/basic*.png & -``` - -### Mac OS - -Refer to [the Linux instructions](#linux), except you need to replace `csplit` -with `gcsplit`. The Mac OS -`csplit` supports a far smaller subset of options. You can install `gcsplit` on -your machine with Homebrew with the command `brew install coreutils`. diff --git a/src/sql/src/query_model/attribute/core.rs b/src/sql/src/query_model/attribute/core.rs deleted file mode 100644 index 78ca31ce5aa8..000000000000 --- a/src/sql/src/query_model/attribute/core.rs +++ /dev/null @@ -1,347 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -//! Framework for modeling and computing derived attributes for QGM -//! graphs. -//! -//! A derived attribute is a value that is associated with a specific -//! QGM box and can be derived from a QGM graph and other derived -//! attributes. -//! -//! To implement a new attribute, define a new type to represent that -//! attribute and implement the [`Attribute`] and [`AttributeKey`] -//! for that type. -//! -//! Note that the current implementation does not support parameterized -//! [`Attribute`] instances, so `MyAttr` is OK, but `MyAttr(i32)` isn't. - -use crate::query_model::model::{BoxId, Model}; -use std::any::type_name; -use std::collections::HashSet; -use std::hash::Hash; -use std::marker::PhantomData; -use typemap_rev::{TypeMap, TypeMapKey}; - -/// A container for derived attributes associated with a specific QGM -/// box. -pub struct Attributes(TypeMap); - -impl std::fmt::Debug for Attributes { - fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Ok(()) // FIXME - } -} - -/// A public api for getting and setting attributes. -impl Attributes { - pub(crate) fn new() -> Self { - Self(TypeMap::new()) - } - - /// Return a value for the attribute `A`. - /// - /// # Panics - /// - /// Panics if the attribute `A` is not set. - #[allow(dead_code)] - pub(crate) fn get(&self) -> &A::Value - where - A::Value: std::fmt::Debug, - { - match self.0.get::>() { - Some(value) => value, - None => panic!("attribute {} not present", type_name::()), - } - } - - /// Set attribute `A` to `value`. - #[allow(dead_code)] - pub(crate) fn set(&mut self, value: A::Value) - where - A::Value: std::fmt::Debug, - { - self.0.insert::>(value); - } -} - -/// A trait that defines the logic for deriving an attribute. -pub(crate) trait Attribute: std::fmt::Debug + 'static { - /// A globally unique identifier for this attribute type. - fn attr_id(&self) -> &'static str; - - /// A vector of attributes that need to be derived before - /// this attribute. - fn requires(&self) -> Vec>; - - /// A function that derives at a specific [`BoxId`]. - fn derive(&self, model: &mut Model, box_id: BoxId); -} - -/// A naive [`PartialEq`] implementation for [`Attribute`] trait objects that -/// differentiates two attributes based on their [`std::any::TypeId`]. -impl PartialEq for dyn Attribute { - fn eq(&self, other: &Self) -> bool { - self.attr_id() == other.attr_id() - } -} - -/// An evidence that the [`PartialEq`] implementation for [`Attribute`] is an -/// equivalence relation. -impl Eq for dyn Attribute {} - -/// A naive `Hash` for attributes that delegates to the associated -/// [`std::any::TypeId`]. -impl Hash for dyn Attribute { - fn hash(&self, state: &mut H) { - self.attr_id().hash(state); - } -} - -/// A trait sets an attribute `Value` type. -pub(crate) trait AttributeKey: Attribute { - type Value: Send + Sync; -} - -/// Helper struct to derive a [`TypeMapKey`] from an [`Attribute`]. -struct AsKey(PhantomData); - -/// Derive a [`TypeMapKey`] from an [`Attribute`]. -impl TypeMapKey for AsKey { - type Value = A::Value; -} - -/// A struct that represents an [`Attribute`] set that needs -/// to be present for some follow-up logic (most likely -/// transformation, but can also be pretty-printing or something -/// else). -pub(crate) struct RequiredAttributes { - attributes: Vec>, -} - -impl From>> for RequiredAttributes { - /// Completes the set attributes with transitive dependencies - /// and wraps the result in a representation that is suitable - /// for attribute derivation in a minimum number of passes. - fn from(mut attributes: HashSet>) -> Self { - // add missing dependencies required to derive this set of attributes - transitive_closure(&mut attributes); - // order transitive closure topologically based on dependency order - let attributes = dependency_order(attributes); - // wrap resulting vector a new RequiredAttributes instance - RequiredAttributes { attributes } - } -} - -impl RequiredAttributes { - /// Derive attributes for the entire model. - /// - /// The currently implementation assumes that all attributes - /// can be derived in a single bottom up pass. - pub(crate) fn derive(&self, model: &mut Model, root: BoxId) { - if !self.attributes.is_empty() { - let _ = model.try_visit_mut_pre_post_descendants( - &mut |_, _| -> Result<(), ()> { Ok(()) }, - &mut |m, box_id| -> Result<(), ()> { - for attr in self.attributes.iter() { - attr.derive(m, *box_id); - } - Ok(()) - }, - root, - ); - } - } -} - -/// Consumes a set of attributes and produces a topologically sorted -/// version of the elements in that set based on the dependency -/// information provided by the [`Attribute::requires`] results. -/// -/// We use Kahn's algorithm[^1] to sort the input. -/// -/// [^1]: -fn dependency_order(attributes: HashSet>) -> Vec> { - let mut rest = attributes.into_iter().collect::>(); - let mut seen: HashSet<&'static str> = HashSet::new(); - let mut sort: Vec> = vec![]; - - while !rest.is_empty() { - let (tail, head) = rest.into_iter().partition::, _>(|attr| { - attr.requires() - .into_iter() - .filter(|req| !seen.contains(req.attr_id())) - .next() - .is_some() - }); - rest = tail; - seen.extend(head.iter().map(|attr| attr.attr_id())); - sort.extend(head); - } - - sort -} - -/// Compute the transitive closure of the given set of attributes. -fn transitive_closure(attributes: &mut HashSet>) { - let mut diff = requirements(attributes); - - // iterate until no new attributes can be discovered - while !diff.is_empty() { - attributes.extend(diff); - diff = requirements(attributes); - } -} - -/// Compute the attributes required to derive the given set of `attributes` that are not -/// already in that set. -fn requirements(attributes: &HashSet>) -> HashSet> { - attributes - .iter() - .flat_map(|a| a.requires()) - .filter(|a| !attributes.contains(a)) - .collect::>() -} - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - - use super::*; - - /// Shorthand macros for defining test attributes. - macro_rules! attr_def { - // attribute with dependencies - ($id:ident depending on $($deps:expr),*) => { - #[derive(Debug, PartialEq, Eq, Clone, Hash)] - struct $id; - - impl Attribute for $id { - fn attr_id(&self) -> &'static str { - stringify!($id) - } - - fn requires(&self) -> Vec> { - vec![$(Box::new($deps)),*] - } - - fn derive(&self, _: &mut Model, _: BoxId) {} - } - }; - // attribute without dependencies - ($id:ident) => { - #[derive(Debug, PartialEq, Eq, Clone, Hash)] - struct $id; - - impl Attribute for $id { - fn attr_id(&self) -> &'static str { - stringify!($id) - } - - fn requires(&self) -> Vec> { - vec![] - } - - fn derive(&self, _: &mut Model, _: BoxId) {} - } - }; - } - - /// Shorthand macro for referencing test attributes. - macro_rules! attr_ref { - // attribute without dependencies - ($id:ident) => {{ - let out: Box = Box::new($id); - out - }}; - } - - // Define trivial attributes for testing purposes. The dependencies - // ionduce the following graph (dependencies are always on the left): - // - // C1 --- B2 --- B1 E1 - // ╲-- A2 --- A1 - // D1 --╱ - attr_def!(A1 depending on A2, D1); - attr_def!(A2 depending on C1); - attr_def!(B1 depending on B2); - attr_def!(B2 depending on C1); - attr_def!(C1); - attr_def!(D1); - attr_def!(E1); - - #[test] - fn test_eq() { - assert_eq!(&attr_ref!(A1), &attr_ref!(A1)); - assert_ne!(&attr_ref!(A1), &attr_ref!(B1)); - assert_ne!(&attr_ref!(B1), &attr_ref!(C1)); - } - - #[test] - fn requirements_ok() { - let attrs1 = HashSet::from([attr_ref!(A1), attr_ref!(B1)]); - let attrs2 = HashSet::from([attr_ref!(A2), attr_ref!(B2), attr_ref!(D1)]); - assert_eq!(requirements(&attrs1), attrs2); - } - - #[test] - fn transitive_closure_ok() { - let mut attrs1 = HashSet::from([attr_ref!(A1), attr_ref!(B1)]); - let attrs2 = HashSet::from([ - attr_ref!(A1), - attr_ref!(A2), - attr_ref!(B1), - attr_ref!(B2), - attr_ref!(C1), - attr_ref!(D1), - ]); - transitive_closure(&mut attrs1); - assert_eq!(attrs1, attrs2); - } - - #[test] - fn dependency_ordered_ok() { - let attrs = dependency_order(HashSet::from([ - attr_ref!(A1), - attr_ref!(A2), - attr_ref!(B1), - attr_ref!(B2), - attr_ref!(C1), - attr_ref!(D1), - attr_ref!(E1), - ])); - - let index = attrs - .iter() - .enumerate() - .map(|(i, a)| (a, i)) - .collect::>(); - - // assert that the result is the right size and has the right elements - assert_eq!(attrs.len(), 7); - assert!(index.contains_key(&attr_ref!(A1))); - assert!(index.contains_key(&attr_ref!(A2))); - assert!(index.contains_key(&attr_ref!(B1))); - assert!(index.contains_key(&attr_ref!(B2))); - assert!(index.contains_key(&attr_ref!(C1))); - assert!(index.contains_key(&attr_ref!(D1))); - assert!(index.contains_key(&attr_ref!(E1))); - - // assert that the result is topologically sorted - let a1 = index[&attr_ref!(A1)]; - let a2 = index[&attr_ref!(A2)]; - let b1 = index[&attr_ref!(B1)]; - let b2 = index[&attr_ref!(B2)]; - let c1 = index[&attr_ref!(C1)]; - let d1 = index[&attr_ref!(D1)]; - assert!(a1 > a2 && a1 > d1); - assert!(a2 > c1); - assert!(b1 > b2); - assert!(b2 > c1); - } -} diff --git a/src/sql/src/query_model/attribute/mod.rs b/src/sql/src/query_model/attribute/mod.rs deleted file mode 100644 index b65b54206535..000000000000 --- a/src/sql/src/query_model/attribute/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -//! Derived attributes framework and definitions. - -pub mod core; -pub mod propagated_nulls; -pub mod rejected_nulls; -pub mod relation_type; diff --git a/src/sql/src/query_model/attribute/propagated_nulls.rs b/src/sql/src/query_model/attribute/propagated_nulls.rs deleted file mode 100644 index b446012efc96..000000000000 --- a/src/sql/src/query_model/attribute/propagated_nulls.rs +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -//! Defines the [`PropagatedNulls`] attribute. -//! -//! The attribute value is a vector of column references corresponding -//! to the columns of the associated `QueryBox`. -//! -//! If any of the references is `NULL`, the corresponding column will -//! also be `NULL`. - -use crate::query_model::attribute::core::{Attribute, AttributeKey}; -use crate::query_model::model::{BoxId, BoxScalarExpr, ColumnReference, Model}; -use std::collections::HashSet; - -#[derive(Debug, PartialEq, Eq, Clone, Hash)] -pub(crate) struct PropagatedNulls; - -impl AttributeKey for PropagatedNulls { - type Value = Vec>; -} - -impl Attribute for PropagatedNulls { - fn attr_id(&self) -> &'static str { - "PropagatedNulls" - } - - fn requires(&self) -> Vec> { - vec![] - } - - fn derive(&self, model: &mut Model, box_id: BoxId) { - let mut r#box = model.get_mut_box(box_id); - - let value = r#box - .columns - .iter() - .map(|x| propagated_nulls(&x.expr)) - .collect::>(); - - // TODO: remove this - // println!("|box[{}].columns| = {:?}", box_id, r#box.columns.len()); - // println!("attr[{}] = {:?}", box_id, value); - - r#box.attributes.set::(value); - } -} - -/// Returns all columns that *must* be non-Null for the `expr` to be non-Null. -pub(crate) fn propagated_nulls(expr: &BoxScalarExpr) -> HashSet { - use BoxScalarExpr::*; - let mut result = HashSet::new(); - - expr.try_visit_pre_post( - &mut |expr| { - match expr { - ColumnReference(col) => { - result.insert(col.clone()); - None - } - BaseColumn(..) | Literal(..) | CallUnmaterializable(_) => None, - CallUnary { func, .. } => { - if func.propagates_nulls() { - None - } else { - Some(vec![]) - } - } - CallBinary { func, .. } => { - if func.propagates_nulls() { - None - } else { - Some(vec![]) - } - } - CallVariadic { func, .. } => { - if func.propagates_nulls() { - None - } else { - Some(vec![]) - } - } - // The branches of an if are computed lazily, but the condition is not. - // However, nulls propagate to the condition are cast to false. - // Consequently, we currently don't do anything here. - // TODO: I think we might be able to take use the intersection of the - // results in the two branches. - If { .. } => Some(vec![]), - // TODO the non-null requeriments of an aggregate expression can - // be pused down to, for example, convert an outer join into an - // inner join - Aggregate { .. } => Some(vec![]), - } - }, - &mut |_| (), - ); - - result -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::query_model::model::*; - use crate::query_model::test::util::*; - - #[test] - fn test_derivation() { - let mut model = Model::default(); - let g_id = model.make_box(qgm::get(0).into()); - { - let mut b = model.get_mut_box(g_id); - b.add_column(exp::base(0, typ::int32(true))); - b.add_column(exp::base(1, typ::int32(true))); - b.add_column(exp::base(2, typ::int32(true))); - b.add_column(exp::base(3, typ::int32(true))); - } - - let s_id = model.make_box(Select::default().into()); - let q_id = model.make_quantifier(QuantifierType::FOREACH, g_id, s_id); - { - let mut b = model.get_mut_box(s_id); - // C0: (#0 - #1) + (#2 - #3) - b.add_column(exp::add( - exp::sub(exp::cref(q_id, 0), exp::cref(q_id, 1)), - exp::sub(exp::cref(q_id, 2), exp::cref(q_id, 3)), - )); - // C1: (#0 > #1) || (#2 > #3) - b.add_column(exp::or( - exp::gt(exp::cref(q_id, 0), exp::cref(q_id, 1)), - exp::gt(exp::cref(q_id, 2), exp::cref(q_id, 3)), - )); - // C2: (#0 > #1) && isnull(#1) - b.add_column(exp::and( - exp::gt(exp::cref(q_id, 0), exp::cref(q_id, 1)), - exp::not(exp::isnull(exp::cref(q_id, 1))), - )); - } - - PropagatedNulls.derive(&mut model, s_id); - - { - let s_box = model.get_box(s_id); - - let act_value = s_box.attributes.get::(); - let exp_value = &vec![ - HashSet::from([cref(q_id, 0), cref(q_id, 1), cref(q_id, 2), cref(q_id, 3)]), - HashSet::from([]), - HashSet::from([]), - ]; - - assert_eq!(act_value, exp_value); - } - } -} diff --git a/src/sql/src/query_model/attribute/rejected_nulls.rs b/src/sql/src/query_model/attribute/rejected_nulls.rs deleted file mode 100644 index e1c34ce52255..000000000000 --- a/src/sql/src/query_model/attribute/rejected_nulls.rs +++ /dev/null @@ -1,554 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -//! Defines the [`RejectedNulls`] attribute. -//! -//! The attribute value is a set of column references associated with -//! each `QueryBox`. If any of the references is `NULL`, there is at -//! least one predicate in that box that will be evaluated to `NULL` -//! or `FALSE` (that is, a row with that column will be filtered -//! away). For boxes without predicates, the attribute value is -//! always the empty set. -//! -//! Besides "predicate p rejects nulls in a set of columns C", in the -//! literature this property is also stated as "predicate p is strong -//! with respect to C". - -use super::propagated_nulls::propagated_nulls; -use crate::query_model::attribute::core::{Attribute, AttributeKey}; -use crate::query_model::model::{ - BoxId, BoxScalarExpr, BoxType, ColumnReference, Model, QuantifierType, -}; -use std::collections::HashSet; - -#[derive(Debug, PartialEq, Eq, Clone, Hash)] -pub(crate) struct RejectedNulls; - -impl AttributeKey for RejectedNulls { - type Value = HashSet; -} - -impl Attribute for RejectedNulls { - fn attr_id(&self) -> &'static str { - "RejectedNulls" - } - - fn requires(&self) -> Vec> { - vec![] - } - - fn derive(&self, model: &mut Model, box_id: BoxId) { - let mut r#box = model.get_mut_box(box_id); - let mut value = HashSet::new(); - - match r#box.box_type { - BoxType::Select(ref select) => { - for p in select.predicates.iter() { - rejected_nulls(p, &mut value); - } - } - BoxType::OuterJoin(ref outerjoin) => { - for p in outerjoin.predicates.iter() { - rejected_nulls(p, &mut value); - } - // By definition, preserved sides in outer joins don't filter - // anything, so columns from the corresponding sides cannot - // reject nulls and need to be removed from the result value. - for q in r#box - .input_quantifiers() - .filter(|q| q.quantifier_type == QuantifierType::PRESERVED_FOREACH) - { - value.retain(|c| c.quantifier_id != q.id); - } - } - _ => (), - } - - r#box.attributes.set::(value); - } -} - -/// Returns all columns that *must* be non-NULL for the boolean `expr` -/// to be `NULL` or `FALSE`. -/// -/// An expression `expr` rejects nulls in a set of column references -/// `C` if it evaluates to either `FALSE` or `NULL` whenever some -/// `c` in `C` is null. -/// -/// An expression `expr` propagates nulls in a set of column references -/// `C` if it evaluates to `NULL` whenever some `c` in `C` is null. -/// -/// Consequently, results returned by [`propagated_nulls`] must be -/// included in [`rejected_nulls`]. -/// -/// Unfortunately, boolean functions such as "and" and "or" are not -/// propagating nulls in their inputs, but we still need to handle -/// them here, as they are used quite frequently in predicates. -/// The procedure for doing this is derived below. -/// -/// Observe the truth values for the following terms: -/// -/// For `AND(A, B)`: -/// -/// | | F | N | T | -/// | |:-:|:-:|:-:| -/// | F | F | F | F | -/// | N | F | N | N | -/// | T | F | N | T | -/// -/// For `OR(A, B)`: -/// -/// | | F | N | T | -/// | |:-:|:-:|:-:| -/// | F | F | N | T | -/// | N | N | N | T | -/// | T | T | T | T | -/// -/// For `NOT(AND(A, B))`: -/// -/// | | F | N | T | -/// | |:-:|:-:|:-:| -/// | F | T | T | T | -/// | N | T | N | N | -/// | T | T | N | F | -/// -/// For `NOT(OR(A, B))`: -/// -/// | | F | N | T | -/// | |:-:|:-:|:-:| -/// | F | T | N | F | -/// | N | N | N | F | -/// | T | F | F | F | -/// -/// Based on the above truth tables, we can establish the following -/// statements are always true: -/// 1. If either `A` or `B` rejects nulls in `C`, -/// then `AND(A, B)` rejects nulls in `C`. -/// 2. If both `A` and `B` reject nulls in `C`, -/// then `OR(A, B)` rejects nulls in `C`. -/// 3. If both `A` and `B` propagate nulls in `C`, -/// then `NOT(AND(A, B))` rejects nulls in `C`. -/// 4. If either `A` or `B` propagates nulls in `C`, -/// then `NOT(OR(A, B))` rejects nulls in `C`. -/// -/// Based on the above statements, the algorithm implemented by -/// this function can be described by the following pseudo-code: -/// -/// ```text -/// def rejected_nulls(expr: Expr, sign: bool = true) -> Set[Expr]: -/// match expr: -/// case NOT(ISNULL(c)): -/// { c } -/// case NOT(expr): -/// rejected_nulls(expr, !sign) -/// case AND(lhs, rhs): -/// if sign > 0: -/// rejected_nulls(lhs, sign) ∪ rejected_nulls(rhs, sign) -/// else: -/// propagated_nulls(lhs) ∩ propagated_nulls(rhs) -/// case OR(lhs, rhs): -/// if sign > 0: -/// rejected_nulls(lhs, sign) ∩ rejected_nulls(rhs, sign) -/// else: -/// propagated_nulls(lhs) ∪ propagated_nulls(rhs) -/// case expr: -/// propagated_nulls(expr) -/// ``` -pub(crate) fn rejected_nulls(expr: &BoxScalarExpr, set: &mut HashSet) { - /// Define an inner function needed in order to pass around the `sign`. - fn rejected_nulls(expr: &BoxScalarExpr, sign: bool) -> HashSet { - mz_ore::stack::maybe_grow(|| { - if let Some(c) = case_not_isnull(expr) { - HashSet::from([c.clone()]) - } else if let Some(expr) = case_not(expr) { - rejected_nulls(expr, !sign) - } else if let Some(exprs) = case_and(expr) { - if sign { - union_variadic(exprs.iter().map(|e| rejected_nulls(e, sign)).collect()) - } else { - intersect_variadic(exprs.iter().map(propagated_nulls).collect()) - } - } else if let Some(exprs) = case_or(expr) { - if sign { - intersect_variadic(exprs.iter().map(|e| rejected_nulls(e, sign)).collect()) - } else { - union_variadic(exprs.iter().map(propagated_nulls).collect()) - } - } else { - propagated_nulls(expr) - } - }) - } - - set.extend(rejected_nulls(expr, true)) -} - -/// Computes the union of two sets, consuming both sides -/// and mutating and returning `lhs`. -fn union(mut lhs: HashSet, rhs: HashSet) -> HashSet -where - T: Clone + Eq + std::hash::Hash, -{ - lhs.extend(rhs); - lhs -} - -/// Computes the union of a vector of sets. -fn union_variadic(sets: Vec>) -> HashSet -where - T: Clone + Eq + std::hash::Hash, -{ - sets.into_iter().reduce(|s1, s2| union(s1, s2)).unwrap() -} - -/// Computes the intersection of two sets, consuming both sides -/// and mutating and returning `lhs`. -fn intersect(mut lhs: HashSet, rhs: HashSet) -> HashSet -where - T: Clone + Eq + std::hash::Hash, -{ - lhs.retain(|item| rhs.contains(item)); - lhs -} - -/// Computes the intersection of a vector of sets. -fn intersect_variadic(sets: Vec>) -> HashSet -where - T: Clone + Eq + std::hash::Hash, -{ - sets.into_iter().reduce(|s1, s2| intersect(s1, s2)).unwrap() -} - -/// Active pattern match for `NOT(ISNULL(c))` fragments. -fn case_not_isnull(expr: &BoxScalarExpr) -> Option<&ColumnReference> { - use BoxScalarExpr::*; - - if let CallUnary { - func: mz_expr::UnaryFunc::Not(mz_expr::func::Not), - expr, - } = expr - { - if let CallUnary { - func: mz_expr::UnaryFunc::IsNull(mz_expr::func::IsNull), - expr, - } = &**expr - { - if let ColumnReference(c) = &**expr { - return Some(c); - } - } - } - - None -} - -/// Active pattern match for `NOT(expr)` fragments. -fn case_not(expr: &BoxScalarExpr) -> Option<&BoxScalarExpr> { - use BoxScalarExpr::*; - - if let CallUnary { - func: mz_expr::UnaryFunc::Not(mz_expr::func::Not), - expr, - } = expr - { - return Some(expr); - } - - None -} - -/// Active pattern match for `expr1 OR expr2 OR ...` fragments. -fn case_or(expr: &BoxScalarExpr) -> Option<&Vec> { - use BoxScalarExpr::*; - - if let CallVariadic { - func: mz_expr::VariadicFunc::Or, - exprs, - } = expr - { - return Some(exprs); - } - - None -} - -/// Active pattern match for `expr1 AND expr2 AND ...` fragments. -fn case_and(expr: &BoxScalarExpr) -> Option<&Vec> { - use BoxScalarExpr::*; - - if let CallVariadic { - func: mz_expr::VariadicFunc::And, - exprs, - } = expr - { - return Some(exprs); - } - - None -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::query_model::model::*; - use crate::query_model::test::util::*; - - #[test] - fn test_select_1() { - let mut model = Model::default(); - let g_id = add_get(&mut model); - let b_id = add_select(&mut model, g_id, |input| { - vec![ - // P0: (#0 - #1) + (#2 - #3) - exp::add( - exp::sub(exp::cref(input, 0), exp::cref(input, 1)), // {#0, #1} - exp::sub(exp::cref(input, 2), exp::cref(input, 3)), // {#2, #3} - ), // {#0, #1, #2, #3} - ] - }); - - assert_derived_attribute(&mut model, b_id, |r#box| { - HashSet::from([ - cref(input(r#box, 0), 0), - cref(input(r#box, 0), 1), - cref(input(r#box, 0), 2), - cref(input(r#box, 0), 3), - ]) // {#0, #1, #2, #3} - }); - } - - #[test] - fn test_select_2() { - let mut model = Model::default(); - let g_id = add_get(&mut model); - let b_id = add_select(&mut model, g_id, |input| { - vec![ - // P0: (#0 > #1) || (#2 < #3) - exp::and( - exp::gt(exp::cref(input, 0), exp::cref(input, 1)), // {#0, #1} - exp::lt(exp::cref(input, 2), exp::cref(input, 3)), // {#2, #3} - ), // {#0, #1, #2, #3} - ] - }); - - assert_derived_attribute(&mut model, b_id, |r#box| { - HashSet::from([ - cref(input(r#box, 0), 0), - cref(input(r#box, 0), 1), - cref(input(r#box, 0), 2), - cref(input(r#box, 0), 3), - ]) // {#0, #1, #2, #3} - }); - } - - #[test] - fn test_select_3() { - let mut model = Model::default(); - let g_id = add_get(&mut model); - let b_id = add_select(&mut model, g_id, |input| { - vec![ - // P0: OR(NOT(OR(#0 < #1, #1 < #2)), AND(!isnull(#1), AND(#0 = #2, #2 = #3)) - exp::or( - exp::not(exp::or( - exp::lt(exp::cref(input, 0), exp::cref(input, 1)), // { #0, #1 } - exp::lt(exp::cref(input, 1), exp::cref(input, 2)), // { #1, #2 } - )), // { #0, #1, #2 } - exp::and( - exp::not(exp::isnull(exp::cref(input, 1))), // { #1 } - exp::and( - exp::eq(exp::cref(input, 0), exp::cref(input, 2)), // { #0, #2 } - exp::eq(exp::cref(input, 2), exp::cref(input, 3)), // { #2, #3 } - ), // { #0, #2, #3 } - ), // { #0, #1, #2, #3 } - ), // { #0, #1, #2 } - // P1: !isnull(#3) - exp::not(exp::isnull(exp::cref(input, 3))), // { #3 } - ] - }); - - assert_derived_attribute(&mut model, b_id, |r#box| { - HashSet::from([ - cref(input(r#box, 0), 0), - cref(input(r#box, 0), 1), - cref(input(r#box, 0), 2), - cref(input(r#box, 0), 3), - ]) // {#0, #1, #2, #3} - }); - } - - #[test] - fn test_select_4() { - let mut model = Model::default(); - let g_id = add_get(&mut model); - let b_id = add_select(&mut model, g_id, |input| { - vec![ - // P0: AND(NOT(AND(#0 < #1, #1 < #2)), OR(!isnull(#2), OR(#0 = #2, #2 = #3)) - exp::and( - exp::not(exp::and( - exp::lt(exp::cref(input, 0), exp::cref(input, 1)), // { #0, #1 } - exp::lt(exp::cref(input, 1), exp::cref(input, 2)), // { #1, #2 } - )), // { #1 } - exp::or( - exp::not(exp::isnull(exp::cref(input, 2))), // { #2 } - exp::or( - exp::eq(exp::cref(input, 0), exp::cref(input, 2)), // { #0, #2 } - exp::eq(exp::cref(input, 2), exp::cref(input, 3)), // { #2, #3 } - ), // { #2 } - ), // { #2 } - ), // { #1, #2 } - // P1: !isnull(#3) - exp::not(exp::isnull(exp::cref(input, 3))), // { #3 } - ] - }); - - assert_derived_attribute(&mut model, b_id, |r#box| { - HashSet::from([ - cref(input(r#box, 0), 1), - cref(input(r#box, 0), 2), - cref(input(r#box, 0), 3), - ]) // {#0, #2, #3} - }); - } - - #[test] - fn test_left_outer_join_1() { - let mut model = Model::default(); - let g_id = add_get(&mut model); - let b_id = add_left_outer_join(&mut model, g_id, g_id, |lhs, rhs| { - vec![ - // P0: (lhs.#0 > rhs.#0) - exp::gt(exp::cref(lhs, 0), exp::cref(rhs, 0)), - // P1: (lhs.#1 < rhs.#1) - exp::lt(exp::cref(lhs, 1), exp::cref(rhs, 1)), - // P2: (lhs.#2 == rhs.#2) - exp::eq(exp::cref(lhs, 2), exp::cref(rhs, 2)), - ] - }); - - assert_derived_attribute(&mut model, b_id, |r#box| { - HashSet::from([ - cref(input(r#box, 1), 0), - cref(input(r#box, 1), 1), - cref(input(r#box, 1), 2), - ]) // {rhs.#0, rhs.#1, rhs.#2} - }); - } - - #[test] - fn test_full_outer_join_1() { - let mut model = Model::default(); - let g_id = add_get(&mut model); - let b_id = add_full_outer_join(&mut model, g_id, g_id, |lhs, rhs| { - vec![ - // P0: (lhs.#0 >= rhs.#0) - exp::gte(exp::cref(lhs, 0), exp::cref(rhs, 0)), - // P1: (lhs.#1 <= rhs.#1) - exp::lte(exp::cref(lhs, 1), exp::cref(rhs, 1)), - // P2: (lhs.#2 != rhs.#2) - exp::not_eq(exp::cref(lhs, 2), exp::cref(rhs, 2)), - ] - }); - - assert_derived_attribute(&mut model, b_id, |_| { - HashSet::from([]) // {} - }); - } - - /// Adds a get box to the given model with a schema consisting of - /// for 32-bit nullable integers. - fn add_get(model: &mut Model) -> BoxId { - let get_id = model.make_box(qgm::get(0).into()); - - let mut b = model.get_mut_box(get_id); - b.add_column(exp::base(0, typ::int32(true))); - b.add_column(exp::base(1, typ::int32(true))); - b.add_column(exp::base(2, typ::int32(true))); - b.add_column(exp::base(3, typ::int32(true))); - - get_id - } - - /// Adds a select join to the model and attaches the given `predicates`. - /// The select box has a single input connected to the `src_id` box. - fn add_select(model: &mut Model, src_id: BoxId, predicates: F) -> BoxId - where - F: FnOnce(QuantifierId) -> Vec, - { - let tgt_id = model.make_box(Select::default().into()); - let inp_id = model.make_quantifier(QuantifierType::FOREACH, src_id, tgt_id); - - if let BoxType::Select(ref mut b) = model.get_mut_box(tgt_id).box_type { - b.predicates.extend_from_slice(&predicates(inp_id)); - } - - tgt_id - } - - /// Adds a full outer join to the model and attaches the given `predicates`. - /// Both inputs are connected to the `lhs_id` and `rhs_id` boxes. - fn add_full_outer_join( - model: &mut Model, - lhs_id: BoxId, - rhs_id: BoxId, - predicates: F, - ) -> BoxId - where - F: FnOnce(QuantifierId, QuantifierId) -> Vec, - { - let tgt_id = model.make_box(OuterJoin::default().into()); - let lhs_id = model.make_quantifier(QuantifierType::PRESERVED_FOREACH, lhs_id, tgt_id); - let rhs_id = model.make_quantifier(QuantifierType::PRESERVED_FOREACH, rhs_id, tgt_id); - - if let BoxType::OuterJoin(ref mut b) = model.get_mut_box(tgt_id).box_type { - b.predicates.extend_from_slice(&predicates(lhs_id, rhs_id)); - } - - tgt_id - } - - /// Adds a left outer join to the model and attaches the given `predicates`. - /// Both inputs are connected to the `lhs_id` and `rhs_id` boxes. - fn add_left_outer_join( - model: &mut Model, - lhs_id: BoxId, - rhs_id: BoxId, - predicates: F, - ) -> BoxId - where - F: FnOnce(QuantifierId, QuantifierId) -> Vec, - { - let tgt_id = model.make_box(OuterJoin::default().into()); - let lhs_id = model.make_quantifier(QuantifierType::PRESERVED_FOREACH, lhs_id, tgt_id); - let rhs_id = model.make_quantifier(QuantifierType::FOREACH, rhs_id, tgt_id); - - if let BoxType::OuterJoin(ref mut b) = model.get_mut_box(tgt_id).box_type { - b.predicates.extend_from_slice(&predicates(lhs_id, rhs_id)); - } - - tgt_id - } - - /// Derives the `RejectedNulls` for the given `b_id` and asserts its value. - fn assert_derived_attribute(model: &mut Model, b_id: BoxId, exp_value: F) - where - F: FnOnce(&QueryBox) -> HashSet, - { - RejectedNulls.derive(model, b_id); - - let r#box = model.get_box(b_id); - let act_value = r#box.attributes.get::(); - let exp_value = &exp_value(&r#box); - - assert_eq!(act_value, exp_value); - } - - fn input(b: &QueryBox, i: usize) -> QuantifierId { - b.quantifiers.iter().nth(i).unwrap().clone() - } -} diff --git a/src/sql/src/query_model/attribute/relation_type.rs b/src/sql/src/query_model/attribute/relation_type.rs deleted file mode 100644 index 71a4e83c6e43..000000000000 --- a/src/sql/src/query_model/attribute/relation_type.rs +++ /dev/null @@ -1,516 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -//! Defines the [`RelationType`] attribute. -//! -//! The attribute value is a vector of [`ColumnType`] associated with -//! the output boxes of each query box. -//! -//! To derive [`Model::cols_type`] we need to derive [`Model::expr_type`] for -//! each column expression in that box. To derive the [`Model::expr_type`], -//! we need to derive [`Model::cref_type`], which in turn requires deriving -//! [`Model::cols_type`] on the input box of the quantifier associated with -//! the [`Model::cref_type`] argument. -//! -//! Derivation therefore must proceed in a bottom-up manner. -//! -//! The implementation makes use of the derived attributes -//! [`RejectedNulls`] as follows: If a [`ColumnReference`] -//! cannot be null if the enclosing box rejects nulls for it. - -use mz_repr::{ColumnType, ScalarType}; - -use crate::query_model::attribute::core::{Attribute, AttributeKey}; -use crate::query_model::attribute::rejected_nulls::RejectedNulls; -use crate::query_model::model::{ - BoundRef, BoxId, BoxScalarExpr, BoxType, ColumnReference, Model, QuantifierType, QueryBox, -}; - -#[derive(Debug, PartialEq, Eq, Clone, Hash)] -pub(crate) struct RelationType; - -impl AttributeKey for RelationType { - type Value = Vec; -} - -impl Attribute for RelationType { - fn attr_id(&self) -> &'static str { - "RelationType" - } - - fn requires(&self) -> Vec> { - vec![Box::new(RejectedNulls)] - } - - #[allow(unused_variables)] - fn derive(&self, model: &mut Model, box_id: BoxId) { - let value = model.cols_type(box_id); - let mut r#box = model.get_mut_box(box_id); - r#box.attributes.set::(value); - } -} - -/// Helper methods for the derivation of the `RelationType` attribute. -impl Model { - /// Infers a vector of [`ColumnType`] values for the `columns` - /// of the given [`BoxId`]. - /// - /// *Note*: this method should be used only internally in order - /// to derive the [`RelationType`] attribute of a [`Model`]. - fn cols_type(&self, box_id: BoxId) -> Vec { - self.get_box(box_id) - .columns - .iter() - .map(|x| self.expr_type(&x.expr, box_id)) - .collect::>() - } - - /// Infers a [`ColumnType`] corresponding to the given - /// [`BoxScalarExpr`] used within the context of an - /// enclosing [`BoxId`]. - /// - /// *Note*: this method should be used only internally in order - /// to derive the [`RelationType`] attribute of a [`Model`]. - fn expr_type(&self, expr: &BoxScalarExpr, enclosing_box_id: BoxId) -> ColumnType { - use BoxScalarExpr::*; - - // a result stack for storing intermediate types - let mut results = Vec::::new(); - - expr.visit_post(&mut |expr| match expr { - BaseColumn(c) => results.push(c.column_type.clone()), - ColumnReference(c) => results.push(self.cref_type(c, enclosing_box_id)), - Literal(_, typ) => results.push(typ.clone()), - CallUnmaterializable(func) => results.push(func.output_type()), - CallUnary { func, .. } => { - let input_type = results.pop().unwrap(); - results.push(func.output_type(input_type)); - } - CallBinary { func, .. } => { - let input2_type = results.pop().unwrap(); - let input1_type = results.pop().unwrap(); - results.push(func.output_type(input1_type, input2_type)); - } - CallVariadic { exprs, func } => { - let input_types = exprs.iter().map(|_| results.pop().unwrap()).rev().collect(); - results.push(func.output_type(input_types)); - } - If { .. } => { - let else_type = results.pop().unwrap(); - let then_type = results.pop().unwrap(); - let cond_type = results.pop().unwrap(); - debug_assert!(then_type.scalar_type.base_eq(&else_type.scalar_type)); - debug_assert!(cond_type.scalar_type.base_eq(&ScalarType::Bool)); - results.push(ColumnType { - scalar_type: then_type.scalar_type, - nullable: then_type.nullable || else_type.nullable, - }); - } - Aggregate { func, .. } => { - let input_type = results.pop().unwrap(); - results.push(func.output_type(input_type)); - } - }); - - debug_assert!( - results.len() == 1, - "expr_type: unexpected results stack size (expected 1, got {})", - results.len() - ); - - results.pop().unwrap() - } - - /// Looks up a [`ColumnType`] corresponding to the given - /// [`ColumnReference`] from the [`RelationType`] attribute - /// of the input box, assuming the [`ColumnReference`] - /// occurs within the context of the enclosing [`BoxId`]. - /// - /// *Note*: this method should be used only internally in order - /// to derive the [`RelationType`] attribute of a [`Model`]. - fn cref_type(&self, cref: &ColumnReference, enclosing_box_id: BoxId) -> ColumnType { - let quantifier = self.get_quantifier(cref.quantifier_id); - - // the type of All and Existential quantifiers is always BOOLEAN NOT NULL - if matches!( - quantifier.quantifier_type, - QuantifierType::EXISTENTIAL | QuantifierType::ALL - ) { - return ColumnType { - scalar_type: ScalarType::Bool, - nullable: false, - }; - } - - let input_box = quantifier.input_box(); - let column_type = &input_box.attributes.get::()[cref.position]; - - let parent_box = self.get_box(quantifier.parent_box); - let nullable = if enclosing_box_id == parent_box.id { - if quantifier.quantifier_type == QuantifierType::SCALAR { - // scalar quantifiers can always be NULL if the subquery is empty - true - } else { - use BoxType::*; - // In general, nullability is influenced by the parent box of the quantifier. - match &parent_box.box_type { - // The type can be restricted to NOT NULL if it is used in a Select - // box with a predicate that rejects nulls in that ColumnReference. - Select(..) if select_rejects_nulls(&parent_box, cref) => false, - // The type should be widened to NULL if it is used in an OuterJoin - // box where the opposite side is preserving. - OuterJoin(..) if outer_join_introduces_nulls(&parent_box, cref) => true, - // The type of aggregates in grouping boxes without keys (a.k.a. - // global aggregates) can always be NULL. This works correctly only - // under the assumption that the output columns of a Grouping box - // are always trivial ColumnReference expressions. - Grouping(grouping) if grouping.key.is_empty() => true, - // The type should be widened to NULL if it is used in a Union - // box with at least one quantifier in the same position being NULL. - Union if union_introduces_nulls(&parent_box, cref) => true, - // The type should be restricted to NOT NULL if it is used in an - // Intersect box with at least one quantifier in the same position - // being NOT NULL. - Intersect if intersect_rejects_nulls(&parent_box, cref) => false, - // otherwise, just inherit the nullable information from the input - _ => column_type.nullable.clone(), - } - } - } else { - // If the enclosing box is different from the parent box (which - // can happen in a graph derived from a correlated subquery), the - // RelationType of the latter might not be fully derived. In this - // case, we therefore just inherit the nullability of the input. - column_type.nullable.clone() - }; - - ColumnType { - scalar_type: column_type.scalar_type.clone(), - nullable, - } - } -} - -/// A [`ColumnReference`] rejects nulls if: -/// 1. the enclosing box is of type [`BoxType::Select`] (asserted), and -/// 2. the [`RejectedNulls`] attribute rejects nulls in the [`ColumnReference`]. -#[inline] -fn select_rejects_nulls(r#box: &BoundRef<'_, QueryBox>, cref: &ColumnReference) -> bool { - debug_assert!(matches!(r#box.box_type, BoxType::Select(..))); - r#box.attributes.get::().contains(cref) -} - -/// A quantifier referenced the given [`ColumnReference`] introduces nulls if: -/// 1. the enclosing box is of type [`BoxType::OuterJoin`] (asserted), and -/// 2. the other quantifier is preserving. -#[inline] -fn outer_join_introduces_nulls(r#box: &BoundRef<'_, QueryBox>, cref: &ColumnReference) -> bool { - debug_assert!(matches!(r#box.box_type, BoxType::OuterJoin(..))); - r#box - .input_quantifiers() - .filter(|q| q.id != cref.quantifier_id) - .any(|q| q.quantifier_type == QuantifierType::PRESERVED_FOREACH) -} - -/// A quantifier referenced the given [`ColumnReference`] introduces nulls if: -/// 1. the enclosing box is of type [`BoxType::Union`] (asserted), and -/// 2. at least one of the input quantifiers for the given position is nullable. -#[inline] -fn union_introduces_nulls(r#box: &BoundRef<'_, QueryBox>, cref: &ColumnReference) -> bool { - debug_assert!(matches!(r#box.box_type, BoxType::Union)); - r#box - .input_quantifiers() - .any(|q| q.input_box().attributes.get::()[cref.position].nullable) -} - -/// A quantifier referenced the given [`ColumnReference`] introduces nulls if: -/// 1. the enclosing box is of type [`BoxType::Intersect`] (asserted), and -/// 2. at least one of the input quantifiers for the given position is not nullable. -#[inline] -fn intersect_rejects_nulls(r#box: &BoundRef<'_, QueryBox>, cref: &ColumnReference) -> bool { - debug_assert!(matches!(r#box.box_type, BoxType::Intersect)); - r#box - .input_quantifiers() - .any(|q| !q.input_box().attributes.get::()[cref.position].nullable) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::query_model::model::*; - use crate::query_model::test::util::*; - - #[test] - fn test_select() { - let mut model = Model::default(); - let g_id = model.make_box(qgm::get(0).into()); - { - let mut b = model.get_mut_box(g_id); - b.add_column(exp::base(0, typ::int32(false))); - b.add_column(exp::base(1, typ::int32(true))); - b.add_column(exp::base(2, typ::int32(true))); - b.add_column(exp::base(3, typ::int32(true))); - b.add_column(exp::base(4, typ::int32(false))); - } - - let s_id = model.make_box(Select::default().into()); - let q_id = model.make_quantifier(QuantifierType::FOREACH, g_id, s_id); - { - let mut b = model.get_mut_box(s_id); - // C0: (#0 + #1) - b.add_column(exp::add(exp::cref(q_id, 0), exp::cref(q_id, 1))); - // C0: (#1 - #2) - b.add_column(exp::sub(exp::cref(q_id, 1), exp::cref(q_id, 2))); - // C0: (#2 * #3) - b.add_column(exp::mul(exp::cref(q_id, 2), exp::cref(q_id, 3))); - // C0: (#3 / #4) - b.add_column(exp::div(exp::cref(q_id, 3), exp::cref(q_id, 4))); - - if let BoxType::Select(s) = &mut b.box_type { - s.predicates.extend_from_slice(&[ - exp::gt(exp::cref(q_id, 1), exp::lit::int32(5)), - exp::not(exp::isnull(exp::cref(q_id, 2))), - ]); - } - } - - // derive Get box (#0) - RejectedNulls.derive(&mut model, g_id); - RelationType.derive(&mut model, g_id); - // derive Select box (#1) - RejectedNulls.derive(&mut model, s_id); - RelationType.derive(&mut model, s_id); - - { - let b = model.get_box(s_id); - - let act_value = b.attributes.get::(); - let exp_value = &vec![ - typ::int32(false), - typ::int32(false), - typ::int32(true), - typ::int32(true), - ]; - - assert_eq!(act_value, exp_value); - } - } - - #[test] - fn test_outer_join() { - let mut model = Model::default(); - let g_id = model.make_box(qgm::get(0).into()); - { - let mut b = model.get_mut_box(g_id); - b.add_column(exp::base(0, typ::int32(false))); - b.add_column(exp::base(1, typ::int32(false))); - b.add_column(exp::base(2, typ::int32(false))); - b.add_column(exp::base(3, typ::int32(false))); - b.add_column(exp::base(4, typ::int32(false))); - } - - let o_id = model.make_box(OuterJoin::default().into()); - let l_id = model.make_quantifier(QuantifierType::PRESERVED_FOREACH, g_id, o_id); - let r_id = model.make_quantifier(QuantifierType::FOREACH, g_id, o_id); - { - let mut b = model.get_mut_box(o_id); - // C0: Q0.0 - b.add_column(exp::cref(l_id, 0)); - // C1: Q0.1 - b.add_column(exp::cref(l_id, 1)); - // C2: Q1.3 - b.add_column(exp::cref(r_id, 3)); - // C3: Q1.4 - b.add_column(exp::cref(r_id, 4)); - } - - // derive Get box (#0) - RejectedNulls.derive(&mut model, g_id); - RelationType.derive(&mut model, g_id); - // derive OuterJoin box (#1) - RejectedNulls.derive(&mut model, o_id); - RelationType.derive(&mut model, o_id); - - { - let b = model.get_box(o_id); - - let act_value = b.attributes.get::(); - let exp_value = &vec![ - typ::int32(false), - typ::int32(false), - typ::int32(true), - typ::int32(true), - ]; - - assert_eq!(act_value, exp_value); - } - } - - #[test] - fn test_union() { - let mut model = Model::default(); - - let g_ids = (0..=2) - .map(|id| { - let g_id = model.make_box(qgm::get(id).into()); - let mut b = model.get_mut_box(g_id); - b.add_column(exp::base(0, typ::int32(false))); - b.add_column(exp::base(1, typ::int32(id == 1))); - b.add_column(exp::base(2, typ::int32(id == 2))); - b.add_column(exp::base(3, typ::int32(id == 1))); - b.add_column(exp::base(4, typ::int32(id == 2))); - g_id - }) - .collect::>(); - - let u_id = model.make_box(BoxType::Union); - let q_ids = g_ids - .iter() - .map(|g_id| model.make_quantifier(QuantifierType::FOREACH, *g_id, u_id)) - .collect::>(); - - { - let mut b = model.get_mut_box(u_id); - // C0: Q0.0 - b.add_column(exp::cref(q_ids[0], 0)); - // C1: Q0.1 - b.add_column(exp::cref(q_ids[0], 1)); - // C2: Q0.2 - b.add_column(exp::cref(q_ids[0], 2)); - // C3: Q0.3 - b.add_column(exp::cref(q_ids[0], 3)); - // C4: Q0.4 - b.add_column(exp::cref(q_ids[0], 4)); - } - - // derive Get box (#i) - for g_id in g_ids { - RejectedNulls.derive(&mut model, g_id); - RelationType.derive(&mut model, g_id); - } - // derive OuterJoin box (#1) - RejectedNulls.derive(&mut model, u_id); - RelationType.derive(&mut model, u_id); - - { - let b = model.get_box(u_id); - - let act_value = b.attributes.get::(); - let exp_value = &vec![ - typ::int32(false), - typ::int32(true), - typ::int32(true), - typ::int32(true), - typ::int32(true), - ]; - - assert_eq!(act_value, exp_value); - } - } - - #[test] - fn test_exists_subquery() { - let mut model = Model::default(); - - let g_ids = (0..=1) - .map(|id| { - let g_id = model.make_box(qgm::get(id).into()); - let mut b = model.get_mut_box(g_id); - b.add_column(exp::base(0, typ::int32(true))); - g_id - }) - .collect::>(); - - let s_id = model.make_box(Select::default().into()); - let q_ids = vec![ - model.make_quantifier(QuantifierType::FOREACH, g_ids[0], s_id), - model.make_quantifier(QuantifierType::EXISTENTIAL, g_ids[1], s_id), - ]; - { - let mut b = model.get_mut_box(s_id); - // C0: Q0.0 - b.add_column(exp::cref(q_ids[0], 0)); - // C0: Q1.0 - b.add_column(exp::cref(q_ids[1], 0)); - } - - // derive Get box (#i) - for g_id in g_ids { - RejectedNulls.derive(&mut model, g_id); - RelationType.derive(&mut model, g_id); - } - // derive Select box (#1) - RejectedNulls.derive(&mut model, s_id); - RelationType.derive(&mut model, s_id); - - { - let b = model.get_box(s_id); - - let act_value = b.attributes.get::(); - let exp_value = &vec![typ::int32(true), typ::bool(false)]; - - assert_eq!(act_value, exp_value); - } - } - - #[test] - fn test_scalar_subquery() { - let mut model = Model::default(); - - let g_ids = (0..=1) - .map(|id| { - let g_id = model.make_box(qgm::get(id).into()); - let mut b = model.get_mut_box(g_id); - b.add_column(exp::base(0, typ::int32(false))); - b.add_column(exp::base(0, typ::bool(false))); - g_id - }) - .collect::>(); - - let s_id = model.make_box(Select::default().into()); - let q_ids = vec![ - model.make_quantifier(QuantifierType::FOREACH, g_ids[0], s_id), - model.make_quantifier(QuantifierType::SCALAR, g_ids[1], s_id), - ]; - { - let mut b = model.get_mut_box(s_id); - // C0: Q0.0 - b.add_column(exp::cref(q_ids[0], 0)); - // C0: Q0.1 - b.add_column(exp::cref(q_ids[0], 1)); - // C0: Q1.0 - b.add_column(exp::cref(q_ids[1], 0)); - // C0: Q1.1 - b.add_column(exp::cref(q_ids[1], 1)); - } - - // derive Get box (#i) - for g_id in g_ids { - RejectedNulls.derive(&mut model, g_id); - RelationType.derive(&mut model, g_id); - } - // derive Select box (#1) - RejectedNulls.derive(&mut model, s_id); - RelationType.derive(&mut model, s_id); - - { - let b = model.get_box(s_id); - - let act_value = b.attributes.get::(); - let exp_value = &vec![ - typ::int32(false), - typ::bool(false), - typ::int32(true), - typ::bool(true), - ]; - - assert_eq!(act_value, exp_value); - } - } -} diff --git a/src/sql/src/query_model/dot.rs b/src/sql/src/query_model/dot.rs deleted file mode 100644 index 474254cc2c4f..000000000000 --- a/src/sql/src/query_model/dot.rs +++ /dev/null @@ -1,362 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -//! Generates a graphviz graph from a Query Graph Model. -//! -//! The public interface consists of the [`Model::as_dot`] method. - -use crate::query_model::model::{ - BoxId, BoxType, ColumnReference, Quantifier, QuantifierId, QueryBox, -}; -use crate::query_model::Model; -use itertools::Itertools; -use mz_ore::str::separated; -use mz_repr::explain_new::ExprHumanizer; -use std::collections::{BTreeMap, HashSet}; -use std::fmt::{self, Write}; - -use super::attribute::core::{Attribute, RequiredAttributes}; -use super::attribute::relation_type::RelationType; - -impl Model { - pub fn as_dot<'a>( - &mut self, - label: &str, - expr_humanizer: &'a dyn ExprHumanizer, - with_types: bool, - ) -> Result { - DotGenerator::new(expr_humanizer, with_types).generate(self, label) - } -} - -/// Generates a graphviz graph from a Query Graph Model, defined in the DOT language. -/// See . -#[derive(Debug)] -struct DotGenerator<'a> { - output: String, - indent: u32, - expr_humanizer: &'a dyn ExprHumanizer, - with_types: bool, -} - -/// Generates a label for a graphviz graph. -#[derive(Debug)] -enum DotLabel<'a> { - /// Plain label - SingleRow(&'a str), - /// A single-column table that has a row for each string in the array. - MultiRow(&'a [String]), -} - -/// Generates a string that escapes characters that would problematic inside the -/// specification for a label. -/// The set of escaped characters is "|{}. -struct DotLabelEscapedString<'a>(&'a str); - -impl<'a> DotGenerator<'a> { - fn new(expr_humanizer: &'a dyn ExprHumanizer, with_types: bool) -> Self { - Self { - output: String::new(), - indent: 0, - expr_humanizer, - with_types, - } - } - - /// Derive attributes required for rendering the given [`Model`]. - fn derive_required_attributes(&self, model: &mut Model, start_box: BoxId) { - // collect a set of required attributes for rendering - let mut attributes = HashSet::>::new(); - if self.with_types { - attributes.insert(Box::new(RelationType)); - } - // derive the required derived attributes - RequiredAttributes::from(attributes).derive(model, start_box); - } - - /// Generates a graphviz graph for the given model, labeled with `label`. - fn generate(self, model: &mut Model, label: &str) -> Result { - self.generate_subgraph(model, model.top_box, label) - } - - /// Generates a graphviz graph for the given subgraph of the model, labeled with `label`. - fn generate_subgraph( - mut self, - model: &mut Model, - start_box: BoxId, - label: &str, - ) -> Result { - self.derive_required_attributes(model, start_box); - - self.new_line("digraph G {"); - self.inc(); - self.new_line("compound = true"); - self.new_line("labeljust = l"); - self.new_line(&DotLabel::SingleRow(label.trim()).to_string()); - self.new_line("node [ shape = box ]"); - - // list of quantifiers for adding the edges connecting them to their - // input boxes after all boxes have been processed - let mut quantifiers = Vec::new(); - - model - .try_visit_pre_post_descendants( - &mut |m, box_id| -> Result<(), ()> { - let b = m.get_box(*box_id); - self.new_line(&format!("subgraph cluster{} {{", box_id)); - self.inc(); - self.new_line( - &DotLabel::SingleRow(&format!("Box{}:{}", box_id, Self::get_box_title(&b))) - .to_string(), - ); - self.new_line(&format!( - "boxhead{} [ shape = record, {} ]", - box_id, - self.get_box_head(&b) - )); - - self.new_line("{"); - self.inc(); - self.new_line("rank = same"); - - if b.input_quantifiers().count() > 0 { - self.new_line("node [ shape = circle ]"); - } - - for q in b.input_quantifiers() { - quantifiers.push(q.id); - - self.new_line(&format!( - "Q{0} [ {1} ]", - q.id, - DotLabel::SingleRow(&format!( - "Q{0}({1}){2}", - q.id, - q.quantifier_type, - Self::get_quantifier_alias(&q) - )) - )); - } - - self.add_correlation_info(b.correlation_info()); - - self.dec(); - self.new_line("}"); - self.dec(); - self.new_line("}"); - - Ok(()) - }, - &mut |_, _| Ok(()), - start_box, - ) - .unwrap(); - - if quantifiers.len() > 0 { - self.new_line("edge [ arrowhead = none, style = dashed ]"); - for q_id in quantifiers.iter() { - let q = model.get_quantifier(*q_id); - self.new_line(&format!( - "Q{0} -> boxhead{1} [ lhead = cluster{1} ]", - q_id, q.input_box - )); - } - } - - self.dec(); - self.new_line("}"); - self.new_line(""); // final empty line - Ok(self.output) - } - - fn get_box_title(b: &QueryBox) -> &'static str { - b.box_type.get_box_type_str() - } - - fn get_box_head(&self, b: &QueryBox) -> String { - let mut rows = Vec::new(); - - rows.push(format!("Distinct: {:?}", b.distinct)); - - // The projection of the box - if self.with_types { - let relation_type = b.attributes.get::(); - for (i, c) in b.columns.iter().enumerate() { - let typ = self.expr_humanizer.humanize_column_type(&relation_type[i]); - if let Some(alias) = &c.alias { - rows.push(format!("{}: {} ({}) as {}", i, c.expr, typ, alias.as_str())); - } else { - rows.push(format!("{}: {} ({})", i, c.expr, typ)); - } - } - } else { - // The projection of the box - for (i, c) in b.columns.iter().enumerate() { - if let Some(alias) = &c.alias { - rows.push(format!("{}: {} as {}", i, c.expr, alias.as_str())); - } else { - rows.push(format!("{}: {}", i, c.expr)); - } - } - } - - // Per-type internal properties. - match &b.box_type { - BoxType::Select(select) => { - if let Some(order_key) = &select.order_key { - rows.push(format!("ORDER BY: {}", separated(", ", order_key.iter()))) - } - } - BoxType::Grouping(grouping) => { - if !grouping.key.is_empty() { - rows.push(format!( - "GROUP BY: {}", - separated(", ", grouping.key.iter()) - )) - } - } - BoxType::Values(values) => { - for row in values.rows.iter() { - rows.push(format!("ROW: {}", separated(", ", row.iter()))) - } - } - BoxType::CallTable(call_table) => { - // display function call - let func = &call_table.func; - let args = &call_table.exprs; - rows.push(format!("CALL: {}({})", func, separated(", ", args))); - } - _ => {} - } - - // @todo predicates as arrows - if let Some(predicates) = match &b.box_type { - BoxType::Select(select) => Some(&select.predicates), - BoxType::OuterJoin(outer_join) => Some(&outer_join.predicates), - _ => None, - } { - rows.extend(predicates.iter().map(|p| p.to_string())); - } - - if let Some(unique_keys) = get_unique_keys(b) { - for key in unique_keys { - let key = key.iter().map(|column| format!("C{}", column)); - rows.push(format!("UNIQUE KEY: {}", separated(", ", key))); - } - } - - DotLabel::MultiRow(&rows).to_string() - } - - /// Adds red arrows from correlated quantifiers to the sibling quantifiers they - /// are correlated with. - fn add_correlation_info( - &mut self, - correlation_info: BTreeMap>, - ) { - let q_correlation_info = correlation_info.into_iter().map(|(id, column_refs)| { - ( - id, - column_refs - .iter() - .map(|c| c.quantifier_id) - .sorted() - .unique() - .collect::>(), - ) - }); - - for (correlated_q, quantifiers) in q_correlation_info { - for q in quantifiers.iter() { - self.new_line(&format!( - "Q{0} -> Q{1} [ {2}, style = filled, color = red ]", - correlated_q, - q, - DotLabel::SingleRow("correlation") - )); - } - } - } - - fn get_quantifier_alias(q: &Quantifier) -> String { - if let Some(alias) = &q.alias { - format!(" as {}", alias) - } else { - "".to_string() - } - } - - fn inc(&mut self) { - self.indent += 1; - } - - fn dec(&mut self) { - self.indent -= 1; - } - - fn new_line(&mut self, s: &str) { - if !self.output.is_empty() && self.output.rfind('\n') != Some(self.output.len()) { - self.end_line(); - for _ in 0..self.indent * 4 { - self.output.push(' '); - } - } - self.output.push_str(s); - } - - fn end_line(&mut self) { - self.output.push('\n'); - } -} - -fn get_unique_keys(b: &QueryBox) -> Option>> { - // TODO: return value of UniqueKeys attribute if present and - // fallback to the Get and CallTable base cases otherwise - // (TBD once the attribute has been added to the codebase) - match &b.box_type { - BoxType::CallTable(call_table) => Some(call_table.func.output_type().keys), - BoxType::Get(get) => Some(get.unique_keys.clone()), - _ => None, - } -} - -impl<'a> fmt::Display for DotLabelEscapedString<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - for c in self.0.chars() { - match c { - '"' => f.write_str("\\\"")?, - '|' => f.write_str("\\|")?, - '{' => f.write_str("\\{")?, - '}' => f.write_str("\\}")?, - '>' => f.write_str("\\>")?, - '<' => f.write_str("\\<")?, - _ => f.write_char(c)?, - } - } - Ok(()) - } -} - -impl<'a> fmt::Display for DotLabel<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("label = \"")?; - match self { - DotLabel::SingleRow(str) => f.write_str(&DotLabelEscapedString(str).to_string()), - DotLabel::MultiRow(strs) => { - f.write_str("{ ")?; - f.write_str(&format!( - "{}", - separated("| ", strs.into_iter().map(|str| DotLabelEscapedString(str))) - ))?; - f.write_str(" }") - } - }?; - f.write_char('\"') - } -} diff --git a/src/sql/src/query_model/error.rs b/src/sql/src/query_model/error.rs deleted file mode 100644 index 89c5f7408934..000000000000 --- a/src/sql/src/query_model/error.rs +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -//! Defines QGM-related errors and traits for those errors. -//! -//! The public interface consists of the [`QGMError`] method and the -//! implemented traits. - -use std::error::Error; -use std::fmt; - -use crate::plan::{HirRelationExpr, HirScalarExpr}; -use crate::query_model::model::{BoxScalarExpr, BoxType, QuantifierType}; - -/// Errors that can occur while handling a QGM model. -/// -/// A bunch of the error types exist because our support for HIR ⇒ QGM -/// conversion and QGM ⇒ MIR conversion is currently incomplete. They will be -/// removed once these limitations are addressed. -#[derive(Debug, Clone)] -pub enum QGMError { - /// Indicates HIR ⇒ QGM conversion failure due to unsupported [`HirRelationExpr`]. - UnsupportedHirRelationExpr(UnsupportedHirRelationExpr), - /// Indicates HIR ⇒ QGM conversion failure due to unsupported [`HirScalarExpr`]. - UnsupportedHirScalarExpr(UnsupportedHirScalarExpr), - /// Indicates QGM ⇒ MIR conversion failure due to unsupported box type. - UnsupportedBoxType(UnsupportedBoxType), - /// Indicates QGM ⇒ MIR conversion failure due to unsupported scalar expression. - UnsupportedBoxScalarExpr(UnsupportedBoxScalarExpr), - /// Indicates QGM ⇒ MIR conversion failure due to unsupported quantifier type. - UnsupportedQuantifierType(UnsupportedQuantifierType), - /// Indicates QGM ⇒ MIR conversion failure due to lack of support for decorrelation. - UnsupportedDecorrelation(UnsupportedDecorrelation), - /// An unstructured error. - Internal(Internal), -} - -impl Error for QGMError {} - -impl fmt::Display for QGMError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - QGMError::UnsupportedHirRelationExpr(e) => write!(f, "{}", e), - QGMError::UnsupportedHirScalarExpr(e) => write!(f, "{}", e), - QGMError::UnsupportedBoxType(e) => write!(f, "{}", e), - QGMError::UnsupportedBoxScalarExpr(e) => write!(f, "{}", e), - QGMError::UnsupportedQuantifierType(e) => write!(f, "{}", e), - QGMError::UnsupportedDecorrelation(e) => write!(f, "{}", e), - QGMError::Internal(e) => write!(f, "{}", e), - } - } -} - -impl From for String { - fn from(error: QGMError) -> Self { - format!("{}", error) - } -} - -#[derive(Debug, Clone)] -pub struct UnsupportedHirRelationExpr { - pub(crate) expr: HirRelationExpr, - pub(crate) explanation: Option, -} - -impl From for QGMError { - fn from(inner: UnsupportedHirRelationExpr) -> Self { - QGMError::UnsupportedHirRelationExpr(inner) - } -} - -impl fmt::Display for UnsupportedHirRelationExpr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "Unsupported HirRelationExpr variant in QGM conversion: {}.", - serde_json::to_string(&self.expr).unwrap() - )?; - if let Some(explanation) = &self.explanation { - write!(f, " Explanation: {}", explanation)?; - } - Ok(()) - } -} - -#[derive(Debug, Clone)] -pub struct UnsupportedHirScalarExpr { - pub(crate) scalar: HirScalarExpr, -} - -impl From for QGMError { - fn from(inner: UnsupportedHirScalarExpr) -> Self { - QGMError::UnsupportedHirScalarExpr(inner) - } -} - -impl fmt::Display for UnsupportedHirScalarExpr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "Unsupported HirScalarExpr variant in QGM conversion: {}", - serde_json::to_string(&self.scalar).unwrap() - ) - } -} - -#[derive(Debug, Clone)] -pub struct UnsupportedBoxType { - pub(crate) box_type: BoxType, - pub(crate) explanation: Option, -} - -impl From for QGMError { - fn from(inner: UnsupportedBoxType) -> Self { - QGMError::UnsupportedBoxType(inner) - } -} - -impl fmt::Display for UnsupportedBoxType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "Unsupported box type in MIR conversion: {:?}.", - self.box_type - )?; - if let Some(explanation) = &self.explanation { - write!(f, " Explanation: {}", explanation)?; - } - Ok(()) - } -} - -#[derive(Debug, Clone)] -pub struct UnsupportedQuantifierType { - pub(crate) quantifier_type: QuantifierType, - /// Context where error is being thrown. - pub(crate) context: String, -} - -impl From for QGMError { - fn from(inner: UnsupportedQuantifierType) -> Self { - QGMError::UnsupportedQuantifierType(inner) - } -} - -impl fmt::Display for UnsupportedQuantifierType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "Unsupported quantifier type in {}: {}.", - self.context, self.quantifier_type - ) - } -} - -#[derive(Debug, Clone)] -pub struct UnsupportedBoxScalarExpr { - pub(crate) scalar: BoxScalarExpr, - /// Context where error is being thrown. - pub(crate) context: String, - pub(crate) explanation: Option, -} - -impl From for QGMError { - fn from(inner: UnsupportedBoxScalarExpr) -> Self { - QGMError::UnsupportedBoxScalarExpr(inner) - } -} - -impl fmt::Display for UnsupportedBoxScalarExpr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "Unsupported QGM scalar expression in {}: {}.", - self.context, self.scalar - )?; - if let Some(explanation) = &self.explanation { - write!(f, " Explanation: {}", explanation)?; - } - Ok(()) - } -} - -#[derive(Debug, Clone)] -pub struct UnsupportedDecorrelation { - pub(crate) msg: String, -} - -impl From for QGMError { - fn from(inner: UnsupportedDecorrelation) -> Self { - QGMError::UnsupportedDecorrelation(inner) - } -} - -impl fmt::Display for UnsupportedDecorrelation { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.msg) - } -} - -#[derive(Debug, Clone)] -pub struct Internal { - pub(crate) msg: String, -} - -impl From for QGMError { - fn from(inner: Internal) -> Self { - QGMError::Internal(inner) - } -} - -impl fmt::Display for Internal { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.msg) - } -} diff --git a/src/sql/src/query_model/hir/hir_from_qgm.rs b/src/sql/src/query_model/hir/hir_from_qgm.rs deleted file mode 100644 index 0900e52f51d3..000000000000 --- a/src/sql/src/query_model/hir/hir_from_qgm.rs +++ /dev/null @@ -1,418 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -//! Generates a [`HirRelationExpr`] from a [`Model`]. -//! -//! The public interface consists of the [`From`] -//! implementation for [`HirRelationExpr`]. - -use std::collections::{HashMap, HashSet}; - -use crate::plan::expr::{ColumnRef, JoinKind}; -use crate::plan::{HirRelationExpr, HirScalarExpr}; -use crate::query_model::attribute::relation_type::RelationType as BoxRelationType; -use crate::query_model::error::*; -use crate::query_model::model::*; -use crate::query_model::{BoxId, Model, QGMError, QuantifierId}; - -use itertools::Itertools; - -use mz_ore::id_gen::IdGen; - -impl TryFrom for HirRelationExpr { - type Error = QGMError; - fn try_from(model: Model) -> Result { - FromModel::default().generate(model) - } -} - -#[derive(Default)] -struct FromModel { - /// Generates [`mz_expr::LocalId`] as an alias for common expressions. - id_gen: IdGen, - // TODO: Add the current correlation information. - // Inspired by FromHir::context_stack. Subject to change. - // context_stack: Vec, - /// Stack of [HirRelationExpr] that have been created through visiting nodes - /// of QGM in post-order. - converted: Vec, - /// Common expressions that have been given a [`mz_expr::LocalId`], from - /// oldest to most recently identified. - lets: Vec<(mz_expr::LocalId, HirRelationExpr, mz_repr::RelationType)>, - /// Map of ([BoxId]s whose HIR representation has been given a - /// [`mz_expr::LocalId`]) -> (position of its HIR representation in `lets`) - common_subgraphs: HashMap, -} - -/// Tracks the column ranges in a [HirRelationExpr] in corresponding the -/// quantifiers in a box. -struct ColumnMap { - quantifier_to_input: HashMap, - input_mapper: mz_expr::JoinInputMapper, -} - -impl FromModel { - fn generate(mut self, mut model: Model) -> Result { - // Derive the RelationType of each box. - use crate::query_model::attribute::core::{Attribute, RequiredAttributes}; - let attributes = HashSet::from_iter(std::iter::once::>(Box::new( - BoxRelationType, - ))); - let root = model.top_box; - RequiredAttributes::from(attributes).derive(&mut model, root); - - // Convert QGM to HIR. - model.try_visit_pre_post( - &mut |_, _| -> Result<(), QGMError> { - // TODO: add to the context stack - Ok(()) - }, - &mut |model, box_id| -> Result<(), QGMError> { self.convert_subgraph(model, box_id) }, - )?; - - debug_assert!(self.converted.len() == 1); - // Retrieve the HIR and put `let`s around it. - let mut result = self.converted.pop().unwrap(); - while let Some((id, value, _)) = self.lets.pop() { - if !matches!(value, HirRelationExpr::Get { .. }) { - result = HirRelationExpr::Let { - name: "".to_string(), - id, - value: Box::new(value), - body: Box::new(result), - } - } - } - Ok(result) - } - - /// Convert subgraph rooted at `box_id` to [HirRelationExpr]. - /// - /// If the box corresponding to `box_id` has multiple ranging quantifiers, - /// the [HirRelationExpr] will be in `self.lets`, and it can be looked up in - /// `self.common_subgraphs`. Otherwise, it will be pushed to `self.converted`. - fn convert_subgraph(&mut self, model: &Model, box_id: &BoxId) -> Result<(), QGMError> { - let r#box = model.get_box(*box_id); - if self.common_subgraphs.get(box_id).is_some() { - // Subgraph has already been converted. - return Ok(()); - } - let hir = match &r#box.box_type { - BoxType::Get(Get { id, unique_keys }) => { - let typ = mz_repr::RelationType::new( - r#box.attributes.get::().to_owned(), - ) - .with_keys(unique_keys.clone()); - HirRelationExpr::Get { - id: mz_expr::Id::Global(*id), - typ, - } - } - BoxType::Values(values) => HirRelationExpr::constant( - // Map `Vec` to `Vec>` - values - .rows - .iter() - .map(|row| { - row.iter() - .map(|expr| { - if let BoxScalarExpr::Literal(datum, _) = expr { - Ok(datum.unpack_first()) - } else { - // TODO: The validator should make this branch - // unreachable. - Err(QGMError::from(UnsupportedBoxScalarExpr { - context: "HIR conversion".to_string(), - scalar: expr.clone(), - explanation: Some("Only literal expressions are supported in Values boxes.".to_string()) - })) - } - }) - .try_collect() - }) - .try_collect()?, - mz_repr::RelationType::new(r#box.attributes.get::().to_owned()), - ), - BoxType::Select(select) => { - let (rels, scalars) = self.convert_quantifiers(&r#box)?; - let (join_q_ids, mut join_inputs): (Vec<_>, Vec<_>) = rels.into_iter().unzip(); - let (map_q_ids, maps): (Vec<_>, Vec<_>) = scalars.into_iter().unzip(); - - let mut result = if let Some(first) = join_inputs.pop() { - // TODO (#11375): determine if this situation should be allowable. - first - } else { - HirRelationExpr::constant(vec![vec![]], mz_repr::RelationType::new(vec![])) - }; - - while let Some(join_input) = join_inputs.pop() { - result = HirRelationExpr::Join { - left: Box::new(result), - right: Box::new(join_input), - kind: JoinKind::Inner, - on: HirScalarExpr::literal_true(), - } - } - if !maps.is_empty() { - result = result.map(maps); - } - let column_map = ColumnMap::new( - Iterator::chain(join_q_ids.into_iter().rev(), map_q_ids.into_iter().rev()), - model, - ); - if !select.predicates.is_empty() { - let filter = select - .predicates - .iter() - .map(|pred| self.convert_scalar(pred, &column_map)) - .filter(|pred| pred != &HirScalarExpr::literal_true()) - .collect_vec(); - if !filter.is_empty() { - result = result.filter(filter); - } - } - let mut result = self.convert_projection(&r#box, &column_map, result); - if r#box.distinct == DistinctOperation::Enforce { - result = result.distinct(); - } - if select.limit.is_some() || select.offset.is_some() || select.order_key.is_some() { - // TODO: put a TopK around the result - unimplemented!() - } - result - } - BoxType::OuterJoin(outer_join) => { - let (mut rels, _) = self.convert_quantifiers(&r#box)?; - let (_, left) = rels.pop().unwrap(); - let (_, right) = rels.pop().unwrap(); - let mut quantifier_iter = r#box.input_quantifiers(); - let left_preserved = matches!( - quantifier_iter.next().unwrap().quantifier_type, - QuantifierType::PRESERVED_FOREACH - ); - let right_preserved = matches!( - quantifier_iter.next().unwrap().quantifier_type, - QuantifierType::PRESERVED_FOREACH - ); - let kind = if left_preserved { - if right_preserved { - JoinKind::FullOuter - } else { - JoinKind::LeftOuter - } - } else if right_preserved { - JoinKind::RightOuter - } else { - JoinKind::Inner - }; - let column_map = ColumnMap::new(r#box.input_quantifiers().map(|q| q.id), model); - let converted_predicates = outer_join - .predicates - .iter() - .map(|pred| self.convert_scalar(pred, &column_map)) - .collect_vec(); - let on = HirScalarExpr::variadic_and(converted_predicates); - let result = HirRelationExpr::Join { - left: Box::new(left), - right: Box::new(right), - kind, - on, - }; - self.convert_projection(&r#box, &column_map, result) - } - _ => unimplemented!(), - }; - - if r#box.ranging_quantifiers().count() > 1 { - let id = mz_expr::LocalId::new(self.id_gen.allocate_id()); - let typ = - mz_repr::RelationType::new(r#box.attributes.get::().to_owned()); - self.common_subgraphs.insert(r#box.id, self.lets.len()); - self.lets.push((id, hir, typ)); - } else { - self.converted.push(hir); - } - Ok(()) - } - - /// Convert the quantifiers of `box` into [HirRelationExpr] and [HirScalarExpr]. - /// - /// Results are returned in reverse quantifier order; the last entry in the - /// second vector corresponds to the first subquery quantifier in the box. - fn convert_quantifiers<'a>( - &mut self, - r#box: &BoundRef<'a, QueryBox>, - ) -> Result< - ( - Vec<(QuantifierId, HirRelationExpr)>, - Vec<(QuantifierId, HirScalarExpr)>, - ), - QGMError, - > { - let mut rels = Vec::new(); - let mut scalars = Vec::new(); - for q in r#box.input_quantifiers().rev() { - // Retrive the HIR corresponding to the input box of the quantifier - let inner_relation = if let Some(let_pos) = self.common_subgraphs.get(&q.input_box) { - if matches!(self.lets[*let_pos].1, HirRelationExpr::Get { .. }) { - self.lets[*let_pos].1.clone() - } else { - HirRelationExpr::Get { - id: mz_expr::Id::Local(self.lets[*let_pos].0), - typ: self.lets[*let_pos].2.clone(), - } - } - } else { - self.converted.pop().unwrap() - }; - - match q.quantifier_type { - QuantifierType::FOREACH | QuantifierType::PRESERVED_FOREACH => { - rels.push((q.id, inner_relation)) - } - QuantifierType::EXISTENTIAL => { - scalars.push((q.id, HirScalarExpr::Exists(Box::new(inner_relation)))) - } - QuantifierType::SCALAR => { - scalars.push((q.id, HirScalarExpr::Select(Box::new(inner_relation)))) - } - _ => { - return Err(QGMError::from(UnsupportedQuantifierType { - quantifier_type: q.quantifier_type, - context: "HIR conversion".to_string(), - })); - } - } - } - Ok((rels, scalars)) - } - - /// Convert the projection of a box into a Map + Project around the - /// [HirRelationExpr] representing the rest of the box. - fn convert_projection<'a>( - &mut self, - r#box: &BoundRef<'a, QueryBox>, - column_map: &ColumnMap, - mut result: HirRelationExpr, - ) -> HirRelationExpr { - let mut map = Vec::new(); - let mut project = Vec::new(); - - for column in &r#box.columns { - let hir_scalar = self.convert_scalar(&column.expr, column_map); - if let HirScalarExpr::Column(ColumnRef { level: 0, column }) = hir_scalar { - // We can skip making a copy of the column. - project.push(column); - } else { - project.push(column_map.arity() + map.len()); - map.push(hir_scalar); - } - } - - if !map.is_empty() { - result = result.map(map); - } - result.project(project) - } - - /// Convert a [BoxScalarExpr] to [HirScalarExpr]. - fn convert_scalar(&mut self, expr: &BoxScalarExpr, column_map: &ColumnMap) -> HirScalarExpr { - let mut results = Vec::new(); - expr.visit_post(&mut |e| match e { - BoxScalarExpr::BaseColumn(BaseColumn { position, .. }) => { - results.push(HirScalarExpr::Column(ColumnRef { - level: 0, - column: *position, - })); - } - BoxScalarExpr::ColumnReference(ColumnReference { - quantifier_id, - position, - }) => { - let converted = if let Some(column) = - column_map.lookup_level_0_col(*quantifier_id, *position) - { - HirScalarExpr::Column(ColumnRef { level: 0, column }) - } else { - // TODO: calculate the level for a correlated subquery. - unimplemented!() - }; - results.push(converted); - } - BoxScalarExpr::Literal(row, typ) => { - results.push(HirScalarExpr::Literal(row.clone(), typ.clone())) - } - BoxScalarExpr::CallUnmaterializable(func) => { - results.push(HirScalarExpr::CallUnmaterializable(func.clone())) - } - BoxScalarExpr::CallUnary { func, .. } => { - let expr = Box::new(results.pop().unwrap()); - results.push(HirScalarExpr::CallUnary { - func: func.clone(), - expr, - }) - } - BoxScalarExpr::CallBinary { func, .. } => { - let expr2 = Box::new(results.pop().unwrap()); - let expr1 = Box::new(results.pop().unwrap()); - results.push(HirScalarExpr::CallBinary { - func: func.clone(), - expr1, - expr2, - }) - } - BoxScalarExpr::CallVariadic { func, exprs } => { - let exprs = results.drain((results.len() - exprs.len())..).collect_vec(); - results.push(HirScalarExpr::CallVariadic { - func: func.clone(), - exprs, - }) - } - BoxScalarExpr::If { .. } => { - let els = Box::new(results.pop().unwrap()); - let then = Box::new(results.pop().unwrap()); - let cond = Box::new(results.pop().unwrap()); - results.push(HirScalarExpr::If { els, then, cond }); - } - _ => unimplemented!(), - }); - debug_assert!(results.len() == 1); - results.pop().unwrap() - } -} - -impl ColumnMap { - fn new(quantifiers: I, model: &Model) -> Self - where - I: Iterator, - { - let (quantifier_to_input, arities): (HashMap<_, _>, Vec<_>) = quantifiers - .enumerate() - .map(|(index, q_id)| ((q_id, index), model.get_quantifier(q_id).output_arity())) - .unzip(); - let input_mapper = mz_expr::JoinInputMapper::new_from_input_arities(arities.into_iter()); - Self { - quantifier_to_input, - input_mapper, - } - } - - fn lookup_level_0_col(&self, q_id: QuantifierId, position: usize) -> Option { - if let Some(input) = self.quantifier_to_input.get(&q_id) { - Some(self.input_mapper.map_column_to_global(position, *input)) - } else { - None - } - } - - #[inline(always)] - fn arity(&self) -> usize { - self.input_mapper.total_columns() - } -} diff --git a/src/sql/src/query_model/hir/mod.rs b/src/sql/src/query_model/hir/mod.rs deleted file mode 100644 index f43624af6ad6..000000000000 --- a/src/sql/src/query_model/hir/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -//! Conversion methods a `Model` to a `HirRelationExpr` and back. -//! -//! The public interface consists of: -//! * `From` implemenation for `Result`. -//! * `From` implemenation for `HirRelationExpr`. - -mod hir_from_qgm; -mod qgm_from_hir; diff --git a/src/sql/src/query_model/hir/qgm_from_hir.rs b/src/sql/src/query_model/hir/qgm_from_hir.rs deleted file mode 100644 index 6c643025e836..000000000000 --- a/src/sql/src/query_model/hir/qgm_from_hir.rs +++ /dev/null @@ -1,553 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -//! Generates a Query Graph Model from a [`HirRelationExpr`]. -//! -//! The public interface consists of the [`From`] -//! implementation for [`Result`]. - -use itertools::Itertools; -use std::collections::HashMap; - -use crate::plan::expr::HirRelationExpr; -use crate::plan::expr::{HirScalarExpr, JoinKind}; -use crate::query_model::error::{QGMError, UnsupportedHirRelationExpr, UnsupportedHirScalarExpr}; -use crate::query_model::model::*; - -impl TryFrom for Model { - type Error = QGMError; - fn try_from(expr: HirRelationExpr) -> Result { - FromHir::default().generate(expr) - } -} - -#[derive(Default)] -struct FromHir { - model: Model, - /// The stack of context boxes for resolving offset-based column references. - context_stack: Vec, - /// Track the `BoxId` that represents each HirRelationExpr::Get expression - /// we have seen so far. - gets_seen: HashMap, -} - -impl FromHir { - /// Generates a Query Graph Model for representing the given query. - fn generate(mut self, expr: HirRelationExpr) -> Result { - self.model.top_box = self.generate_select(expr)?; - Ok(self.model) - } - - /// Generates a sub-graph representing the given expression, ensuring - /// that the resulting graph starts with a Select box. - fn generate_select(&mut self, expr: HirRelationExpr) -> Result { - let mut box_id = self.generate_internal(expr)?; - if !self.model.get_box(box_id).is_select() { - box_id = self.wrap_within_select(box_id); - } - Ok(box_id) - } - - /// Generates a sub-graph representing the given expression. - fn generate_internal(&mut self, expr: HirRelationExpr) -> Result { - match expr { - HirRelationExpr::Constant { rows, typ } => { - if typ.arity() == 0 && rows.len() == 1 { - let values_box_id = self.model.make_box(BoxType::Values(Values { - rows: rows.iter().map(|_| Vec::new()).collect_vec(), - })); - Ok(values_box_id) - } else { - // The only expected constant collection is `{ () }` (the - // singleton collection of a zero-arity tuple) and is handled above. - // In theory, this should be `unreachable!(...)`, but we return an - // `Err` in order to bubble up this to the user without panicking. - Err(QGMError::from(UnsupportedHirRelationExpr { - expr: HirRelationExpr::Constant { rows, typ }, - explanation: Some(String::from("Constant with arity > 0 or length != 1")), - })) - } - } - HirRelationExpr::Get { id, typ } => { - if let Some(box_id) = self.gets_seen.get(&id) { - return Ok(*box_id); - } - if let mz_expr::Id::Global(id) = id { - let result = self.model.make_box(BoxType::Get(Get { - id, - unique_keys: typ.keys, - })); - let mut b = self.model.get_mut_box(result); - self.gets_seen.insert(mz_expr::Id::Global(id), result); - b.columns - .extend(typ.column_types.into_iter().enumerate().map( - |(position, column_type)| Column { - expr: BoxScalarExpr::BaseColumn(BaseColumn { - position, - column_type, - }), - alias: None, - }, - )); - Ok(result) - } else { - // Other id variants should not be present in the HirRelationExpr. - // In theory, this should be `unreachable!(...)`, but we return an - // `Err` in order to bubble up this to the user without panicking. - Err(QGMError::from(UnsupportedHirRelationExpr { - expr: HirRelationExpr::Get { id, typ }, - explanation: Some(String::from("Unexpected Id variant in Get")), - })) - } - } - HirRelationExpr::Let { - name: _, - id, - value, - body, - } => { - let id = mz_expr::Id::Local(id); - let value_box_id = self.generate_internal(*value)?; - let prev_value = self.gets_seen.insert(id.clone(), value_box_id); - let body_box_id = self.generate_internal(*body)?; - if let Some(prev_value) = prev_value { - self.gets_seen.insert(id, prev_value); - } else { - self.gets_seen.remove(&id); - } - Ok(body_box_id) - } - HirRelationExpr::Project { input, outputs } => { - let input_box_id = self.generate_internal(*input)?; - let select_id = self.model.make_select_box(); - let quantifier_id = - self.model - .make_quantifier(QuantifierType::FOREACH, input_box_id, select_id); - let mut select_box = self.model.get_mut_box(select_id); - for position in outputs { - select_box.add_column(BoxScalarExpr::ColumnReference(ColumnReference { - quantifier_id, - position, - })); - } - Ok(select_id) - } - HirRelationExpr::Map { input, mut scalars } => { - let mut box_id = self.generate_internal(*input)?; - // We could install the predicates in `input_box` if it happened - // to be a `Select` box. However, that would require pushing down - // the predicates through its projection, since the predicates are - // written in terms of elements in `input`'s projection. - // Instead, we just install a new `Select` box for holding the - // predicate, and let normalization tranforms simplify the graph. - box_id = self.wrap_within_select(box_id); - - loop { - let old_arity = self.model.get_box(box_id).columns.len(); - - // 1) Find a prefix of scalars such that no scalar in the - // current batch depends on columns from the same batch. - let end_idx = scalars - .iter_mut() - .position(|s| { - let mut requires_nonexistent_column = false; - #[allow(deprecated)] - s.visit_columns(0, &mut |depth, col| { - if col.level == depth { - requires_nonexistent_column |= (col.column + 1) > old_arity - } - }); - requires_nonexistent_column - }) - .unwrap_or(scalars.len()); - - // 2) Add the scalars in the prefix to the box. - for scalar in scalars.drain(0..end_idx) { - let expr = self.generate_expr(scalar, box_id)?; - let mut b = self.model.get_mut_box(box_id); - b.add_column(expr); - } - - // 3) If there are scalars remaining, wrap the box so the - // remaining scalars can point to the scalars in this prefix. - if scalars.is_empty() { - break; - } - box_id = self.wrap_within_select(box_id); - } - Ok(box_id) - } - HirRelationExpr::CallTable { func, exprs } => { - // mark the output type of the called function for later - let output_type = func.output_type(); - - // create box with empty function call argument expressions - let call_table = CallTable::new(func, vec![]); - let box_id = self.model.make_box(call_table.into()); - - // fix function the call arguments (it is a bit awkward that - // this needs to be adapted only after creating the box) - let exprs = self.generate_exprs(exprs, box_id)?; - let mut r#box = self.model.get_mut_box(box_id); - if let BoxType::CallTable(call_table) = &mut r#box.box_type { - call_table.exprs = exprs; - } - - // add the output of the table as projected columns - for (position, column_type) in output_type.column_types.into_iter().enumerate() { - r#box.add_column(BoxScalarExpr::BaseColumn(BaseColumn { - position, - column_type, - })); - } - - Ok(box_id) - } - HirRelationExpr::Filter { input, predicates } => { - let input_box = self.generate_internal(*input)?; - // We could install the predicates in `input_box` if it happened - // to be a `Select` box. However, that would require pushing down - // the predicates through its projection, since the predicates are - // written in terms of elements in `input`'s projection. - // Instead, we just install a new `Select` box for holding the - // predicate, and let normalization tranforms simplify the graph. - let select_id = self.wrap_within_select(input_box); - for predicate in predicates { - let expr = self.generate_expr(predicate, select_id)?; - self.add_predicate(select_id, expr); - } - Ok(select_id) - } - HirRelationExpr::Join { - left, - mut right, - on, - kind, - } => { - let (box_type, left_q_type, right_q_type) = match kind { - JoinKind::Inner { .. } => ( - BoxType::Select(Select::default()), - QuantifierType::FOREACH, - QuantifierType::FOREACH, - ), - JoinKind::LeftOuter { .. } => ( - BoxType::OuterJoin(OuterJoin::default()), - QuantifierType::PRESERVED_FOREACH, - QuantifierType::FOREACH, - ), - JoinKind::RightOuter => ( - BoxType::OuterJoin(OuterJoin::default()), - QuantifierType::FOREACH, - QuantifierType::PRESERVED_FOREACH, - ), - JoinKind::FullOuter => ( - BoxType::OuterJoin(OuterJoin::default()), - QuantifierType::PRESERVED_FOREACH, - QuantifierType::PRESERVED_FOREACH, - ), - }; - let join_box = self.model.make_box(box_type); - - // Left box - let left_box = self.generate_internal(*left)?; - self.model.make_quantifier(left_q_type, left_box, join_box); - - // Right box - let right_box = self.within_context(join_box, &mut |generator| { - let right = right.take(); - generator.generate_internal(right) - })?; - self.model - .make_quantifier(right_q_type, right_box, join_box); - - // ON clause - let predicate = self.generate_expr(on, join_box)?; - self.add_predicate(join_box, predicate); - - // Default projection - self.model.get_mut_box(join_box).add_all_input_columns(); - - Ok(join_box) - } - HirRelationExpr::Reduce { - input, - group_key, - aggregates, - expected_group_size: _, - } => { - // An intermediate Select Box is generated between the input box and - // the resulting grouping box so that the grouping key and the arguments - // of the aggregations contain only column references - let input_box_id = self.generate_internal(*input)?; - let select_id = self.model.make_select_box(); - let input_q_id = - self.model - .make_quantifier(QuantifierType::FOREACH, input_box_id, select_id); - let group_box_id = self - .model - .make_box(BoxType::Grouping(Grouping { key: Vec::new() })); - let select_q_id = - self.model - .make_quantifier(QuantifierType::FOREACH, select_id, group_box_id); - let mut key = Vec::new(); - for k in group_key.into_iter() { - // Make sure the input select box projects the source column needed - let input_col_ref = BoxScalarExpr::ColumnReference(ColumnReference { - quantifier_id: input_q_id, - position: k, - }); - let position = self - .model - .get_mut_box(select_id) - .add_column_if_not_exists(input_col_ref); - // Reference of the column projected by the input select box - let select_box_col_ref = BoxScalarExpr::ColumnReference(ColumnReference { - quantifier_id: select_q_id, - position, - }); - // Add it to the grouping key and to the projection of the - // Grouping box - key.push(select_box_col_ref.clone()); - self.model - .get_mut_box(group_box_id) - .add_column(select_box_col_ref); - } - for aggregate in aggregates.into_iter() { - // Any computed expression passed as an argument of an aggregate - // function is computed by the input select box. - let input_expr = self.generate_expr(*aggregate.expr, select_id)?; - let position = self - .model - .get_mut_box(select_id) - .add_column_if_not_exists(input_expr); - // Reference of the column projected by the input select box - let col_ref = BoxScalarExpr::ColumnReference(ColumnReference { - quantifier_id: select_q_id, - position, - }); - // Add the aggregate expression to the projection of the Grouping - // box - let aggregate = BoxScalarExpr::Aggregate { - func: aggregate.func.into_expr(), - expr: Box::new(col_ref), - distinct: aggregate.distinct, - }; - self.model.get_mut_box(group_box_id).add_column(aggregate); - } - - // Update the key of the grouping box - if let BoxType::Grouping(g) = &mut self.model.get_mut_box(group_box_id).box_type { - g.key.extend(key); - } - Ok(group_box_id) - } - HirRelationExpr::Distinct { input } => { - let select_id = self.generate_select(*input)?; - let mut select_box = self.model.get_mut_box(select_id); - select_box.distinct = DistinctOperation::Enforce; - Ok(select_id) - } - // HirRelationExpr::TopK { - // input, - // group_key, - // order_key, - // limit, - // offset, - // } => todo!(), - // HirRelationExpr::Negate { input } => todo!(), - // HirRelationExpr::Threshold { input } => todo!(), - HirRelationExpr::Union { base, inputs } => { - // recurse to inputs - let mut input_ids = vec![self.generate_internal(*base)?]; - for input in inputs { - input_ids.push(self.generate_internal(input)?); - } - - // create Union box and connect inputs - let union_id = self.model.make_box(BoxType::Union); - let quant_ids = input_ids - .iter() - .map(|input_id| { - self.model - .make_quantifier(QuantifierType::FOREACH, *input_id, union_id) - }) - .collect::>(); - - // project all columns from the first input (convention-based constraint) - let columns_len = self.model.get_box(input_ids[0]).columns.len(); - let mut union_box = self.model.get_mut_box(union_id); - for n in 0..columns_len { - union_box.add_column(BoxScalarExpr::ColumnReference(ColumnReference { - quantifier_id: quant_ids[0], - position: n, - })); - } - - Ok(union_id) - } - expr => Err(QGMError::from(UnsupportedHirRelationExpr { - expr, - explanation: None, - })), - } - } - - /// Returns a Select box ranging over the given box, projecting - /// all of its columns. - fn wrap_within_select(&mut self, box_id: BoxId) -> BoxId { - let select_id = self.model.make_select_box(); - self.model - .make_quantifier(QuantifierType::FOREACH, box_id, select_id); - self.model.get_mut_box(select_id).add_all_input_columns(); - select_id - } - - /// Lowers the given expression within the context of the given box. - /// - /// Note that this method may add new quantifiers to the box for subquery - /// expressions. - fn generate_expr( - &mut self, - expr: HirScalarExpr, - context_box: BoxId, - ) -> Result { - match expr { - HirScalarExpr::Literal(row, col_type) => Ok(BoxScalarExpr::Literal(row, col_type)), - HirScalarExpr::Column(c) => { - let context_box = match c.level { - 0 => context_box, - _ => self.context_stack[self.context_stack.len() - c.level], - }; - Ok(BoxScalarExpr::ColumnReference( - self.find_column_within_box(context_box, c.column), - )) - } - HirScalarExpr::CallUnmaterializable(func) => { - Ok(BoxScalarExpr::CallUnmaterializable(func)) - } - HirScalarExpr::CallUnary { func, expr } => Ok(BoxScalarExpr::CallUnary { - func, - expr: Box::new(self.generate_expr(*expr, context_box)?), - }), - HirScalarExpr::CallBinary { func, expr1, expr2 } => Ok(BoxScalarExpr::CallBinary { - func, - expr1: Box::new(self.generate_expr(*expr1, context_box)?), - expr2: Box::new(self.generate_expr(*expr2, context_box)?), - }), - HirScalarExpr::CallVariadic { func, exprs } => Ok(BoxScalarExpr::CallVariadic { - func, - exprs: exprs - .into_iter() - .map(|expr| self.generate_expr(expr, context_box)) - .collect::, _>>()?, - }), - HirScalarExpr::If { cond, then, els } => Ok(BoxScalarExpr::If { - cond: Box::new(self.generate_expr(*cond, context_box)?), - then: Box::new(self.generate_expr(*then, context_box)?), - els: Box::new(self.generate_expr(*els, context_box)?), - }), - HirScalarExpr::Select(mut expr) => { - let box_id = self.within_context(context_box, &mut move |generator| { - generator.generate_select(expr.take()) - })?; - let quantifier_id = - self.model - .make_quantifier(QuantifierType::SCALAR, box_id, context_box); - Ok(BoxScalarExpr::ColumnReference(ColumnReference { - quantifier_id, - position: 0, - })) - } - HirScalarExpr::Exists(mut expr) => { - let box_id = self.within_context(context_box, &mut move |generator| { - generator.generate_select(expr.take()) - })?; - let quantifier_id = - self.model - .make_quantifier(QuantifierType::EXISTENTIAL, box_id, context_box); - Ok(BoxScalarExpr::ColumnReference(ColumnReference { - quantifier_id, - position: 0, - })) - } - scalar @ HirScalarExpr::Windowing(..) => { - Err(QGMError::from(UnsupportedHirScalarExpr { scalar })) - } - // A Parameter should never be part of the input expression, see - // `expr.bind_parameters(¶ms)?;` calls in `dml::plan_*` and `ddl:plan_*`. - // In theory, this should be `unreachable!(...)`, but we return an - // `Err` in order to bubble up this to the user without panicking. - scalar @ HirScalarExpr::Parameter(..) => { - Err(QGMError::from(UnsupportedHirScalarExpr { scalar })) - } - } - } - - /// Lowers the given expressions within the context of the given box. - /// - /// Delegates to [`FromHir::generate_expr`] for each element. - fn generate_exprs( - &mut self, - exprs: Vec, - context_box: BoxId, - ) -> Result, QGMError> { - exprs - .into_iter() - .map(|expr| self.generate_expr(expr, context_box)) - .collect() - } - - /// Find the N-th column among the columns projected by the input quantifiers - /// of the given box. This method translates Hir's offset-based column into - /// quantifier-based column references. - /// - /// This method is equivalent to `expr::JoinInputMapper::map_column_to_local`, in - /// the sense that given all the columns projected by a join (represented by the - /// set of input quantifiers of the given box) it returns the input the column - /// belongs to and its offset within the projection of the underlying operator. - fn find_column_within_box(&self, box_id: BoxId, mut position: usize) -> ColumnReference { - let b = self.model.get_box(box_id); - for q_id in b.quantifiers.iter() { - let q = self.model.get_quantifier(*q_id); - let ib = self.model.get_box(q.input_box); - if position < ib.columns.len() { - return ColumnReference { - quantifier_id: *q_id, - position, - }; - } - position -= ib.columns.len(); - } - unreachable!("column not found") - } - - /// Executes the given action within the context of the given box. - fn within_context(&mut self, context_box: BoxId, f: &mut F) -> T - where - F: FnMut(&mut Self) -> T, - { - self.context_stack.push(context_box); - let result = f(self); - self.context_stack.pop(); - result - } - - /// Adds the given predicate to the given box. - /// - /// The given box must support predicates, ie. it must be either a Select box - /// or an OuterJoin one. - fn add_predicate(&mut self, box_id: BoxId, predicate: BoxScalarExpr) { - let mut the_box = self.model.get_mut_box(box_id); - match &mut the_box.box_type { - BoxType::Select(select) => select.predicates.push(predicate), - BoxType::OuterJoin(outer_join) => outer_join.predicates.push(predicate), - _ => unreachable!(), - } - } -} diff --git a/src/sql/src/query_model/mir.rs b/src/sql/src/query_model/mir.rs deleted file mode 100644 index 6cdb718db7ca..000000000000 --- a/src/sql/src/query_model/mir.rs +++ /dev/null @@ -1,783 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -//! Converts a Query Graph Model into a [mz_expr::MirRelationExpr]. -//! -//! The public interface consists of the implementation of -//! [`Into`] for [`Model`]. - -use crate::query_model::error::{ - QGMError, UnsupportedBoxScalarExpr, UnsupportedBoxType, UnsupportedDecorrelation, - UnsupportedQuantifierType, -}; -use crate::query_model::model::{ - BaseColumn, BoundRef, BoxId, BoxScalarExpr, BoxType, ColumnReference, DistinctOperation, Get, - Model, QuantifierId, QuantifierSet, QuantifierType, QueryBox, -}; -use itertools::Itertools; -use mz_ore::collections::CollectionExt; -use mz_ore::id_gen::IdGen; -use mz_repr::{Datum, RelationType, ScalarType}; -use std::collections::HashMap; - -impl TryFrom for mz_expr::MirRelationExpr { - type Error = QGMError; - fn try_from(model: Model) -> Result { - Lowerer::new(&model).lower() - } -} - -/// Maps a column reference to a specific column position. -/// -/// This is used for resolving column references when lowering expressions -/// on top of a `MirRelationExpr`, where it maps each column reference with -/// with a column position within the projection of that `MirRelationExpr`. -type ColumnMap = HashMap; - -struct Lowerer<'a> { - model: &'a Model, - /// Generates [`mz_expr::LocalId`] as an alias for common expressions. - id_gen: IdGen, - /// Stack of common expressions that have been given a [`mz_expr::LocalId`]. - lets: Vec<(mz_expr::LocalId, mz_expr::MirRelationExpr)>, - /// Map of (BoxIds whose MIR representation has been given a - /// [`mz_expr::LocalId`]) -> (position of its MIR representation in `lets`) - common_boxes: HashMap, -} - -impl<'a> Lowerer<'a> { - fn new(model: &'a Model) -> Self { - Self { - model, - id_gen: IdGen::default(), - lets: Vec::new(), - common_boxes: HashMap::new(), - } - } - - /// Pushes a `rel`ation to the stack of common expressions. - /// - /// If `rel` is a `mz_expr::MirRelationExpr::Get`, it does not get added to - /// the stack. - /// - /// Returns - /// * the [`mz_expr::MirRelationExpr`] that will retrieve the common - /// expression. - /// * the location of `rel`, if any, within `self.lets`. - fn push_let( - &mut self, - rel: mz_expr::MirRelationExpr, - ) -> (mz_expr::MirRelationExpr, Option) { - if matches!(rel, mz_expr::MirRelationExpr::Get { .. }) { - (rel, None) - } else { - let id = mz_expr::LocalId::new(self.id_gen.allocate_id()); - let get = mz_expr::MirRelationExpr::Get { - id: mz_expr::Id::Local(id), - typ: rel.typ(), - }; - let stack_pos = self.lets.len(); - self.lets.push((id, rel)); - (get, Some(stack_pos)) - } - } - - /// Converts `self.model` to a [`mz_expr::MirRelationExpr`]. - fn lower(&mut self) -> Result { - let (initial_outer, _) = self.push_let(mz_expr::MirRelationExpr::constant( - vec![vec![]], - RelationType::new(vec![]), - )); - let mut result = self.apply(self.model.top_box, initial_outer, &ColumnMap::new())?; - while let Some((id, value)) = self.lets.pop() { - result = mz_expr::MirRelationExpr::Let { - id, - value: Box::new(value), - body: Box::new(result), - } - } - Ok(result) - } - - /// Applies the given box identified by its ID to the given outer relation. - fn apply( - &mut self, - box_id: BoxId, - get_outer: mz_expr::MirRelationExpr, - outer_column_map: &ColumnMap, - ) -> Result { - if let Some(let_pos) = self.common_boxes.get(&box_id) { - return Ok(mz_expr::MirRelationExpr::Get { - id: mz_expr::Id::Local(self.lets[*let_pos].0), - typ: self.lets[*let_pos].1.typ(), - }); - } - - use mz_expr::MirRelationExpr as SR; - let the_box = self.model.get_box(box_id); - - let input = match &the_box.box_type { - BoxType::Get(Get { id, unique_keys }) => { - let typ = RelationType::new( - the_box - .columns - .iter() - .map(|c| { - if let BoxScalarExpr::BaseColumn(BaseColumn { column_type, .. }) = - &c.expr - { - column_type.clone() - } else { - // TODO: the validator should make this branch unreachable. - panic!("expected all columns in Get BoxType to BaseColumn"); - } - }) - .collect::>(), - ) - .with_keys(unique_keys.clone()); - Ok(get_outer.product(SR::Get { - id: mz_expr::Id::Global(*id), - typ, - })) - } - BoxType::Values(values) => { - let identity = SR::constant(vec![vec![]], RelationType::new(vec![])); - // TODO(asenac) lower actual values - if values.rows.len() != 1 || the_box.columns.len() != 0 { - let msg = String::from("Only join identity constants are currently supported."); - return Err(QGMError::from(UnsupportedBoxType { - box_type: the_box.box_type.clone(), - explanation: Some(msg), - })); - } - Ok(get_outer.product(identity)) - } - BoxType::Select(select) => { - if select.order_key.is_some() { - let msg = String::from("ORDER BY is not supported yet"); - return Err(QGMError::from(UnsupportedBoxType { - box_type: the_box.box_type.clone(), - explanation: Some(msg), - })); - } - if select.limit.is_some() || select.offset.is_some() { - let msg = String::from("LIMIT/OFFSET is not supported yet"); - return Err(QGMError::from(UnsupportedBoxType { - box_type: the_box.box_type.clone(), - explanation: Some(msg), - })); - } - // A Select box combines three operations, join, filter and project, - // in that order. - - // 1) Lower the join component of the Select box. - // TODO(asenac) We could join first all non-correlated quantifiers - // and then apply the correlated ones one by one in order of dependency - // on top the join built so far, adding the predicates as soon as their - // dependencies are satisfied. - let correlation_info = the_box.correlation_info(); - if !correlation_info.is_empty() { - let msg = String::from("correlated joins are not supported yet"); - return Err(QGMError::from(UnsupportedDecorrelation { msg })); - } - - let outer_arity = get_outer.arity(); - let (mut input, column_map) = - self.lower_join(get_outer, outer_column_map, &the_box.quantifiers)?; - let input_arity = input.arity(); - - let lowered_predicates: Vec<_> = select - .predicates - .iter() - .map(|p| Self::lower_expression(p, &column_map)) - .try_collect()?; - // 2) Lower the filter component. - if !select.predicates.is_empty() { - input = input.filter(lowered_predicates.into_iter()); - } - - // 3) Lower the project component. - Self::lower_box_columns(input, &the_box, &column_map, outer_arity, input_arity) - } - BoxType::Grouping(grouping) => { - // Reduce may not contain expressions with correlated subqueries, due to the constraints asserted - // by the HIR ⇒ QGM conversion (aggregate and key are pushed to a preceding Select box and - // are always trivial in the Grouping box). - - // In addition, here an empty reduction key signifies that we need to supply default values - // in the case that there are no results (as in a SQL aggregation without an explicit GROUP BY). - - // Note: a grouping box must only contain a single quantifier but we can still - // re-use `lower_join` for single quantifier joins - let (mut input, column_map) = - self.lower_join(get_outer.clone(), outer_column_map, &the_box.quantifiers)?; - - // Build the reduction - let group_key = grouping - .key - .iter() - .map(|k| Self::lower_expression(k, &column_map).unwrap()) - .collect_vec(); - let aggregates = the_box - .columns - .iter() - .filter_map(|c| { - if let BoxScalarExpr::Aggregate { - func, - expr, - distinct, - } = &c.expr - { - Some(mz_expr::AggregateExpr { - func: func.clone(), - expr: Self::lower_expression(expr, &column_map).unwrap(), - distinct: *distinct, - }) - } else { - None - } - }) - .collect_vec(); - - if group_key.is_empty() { - // SQL semantics require default values for global aggregates - let input_type = input.typ(); - let default = aggregates - .iter() - .map(|agg| { - ( - agg.func.default(), - agg.typ(&input_type.column_types).scalar_type, - ) - }) - .collect_vec(); - - input = SR::Reduce { - input: Box::new(input), - group_key, - aggregates, - monotonic: false, - expected_group_size: None, - }; - - Ok(get_outer.lookup(&mut self.id_gen, input, default)) - } else { - // Put the columns in the same order as projected by the Grouping box by - // adding an additional projection. - // - // The aggregate expressions are usually projected after the grouping key. - // However, as a result of query rewrite, aggregate expressions may be - // replaced with grouping key items. For example, the following query: - // - // select a, max(b), max(a) from t group by a; - // - // could be rewritten as: - // - // select a, max(b), a from t group by a; - // - // resulting in a Grouping box projecting [a, max(b), a]. Even though - // another query rewrite should remove duplicated columns in the projection - // of the Grouping box, we should be able to lower any semantically valid - // query graph. - let mut aggregate_count = 0; - let projection = the_box.columns.iter().map(|c| { - if let BoxScalarExpr::Aggregate { .. } = &c.expr { - let aggregate_pos = grouping.key.len() + aggregate_count; - aggregate_count += 1; - aggregate_pos - } else { - grouping - .key - .iter() - .position(|k| c.expr == *k) - .expect("expression in the projection of a Grouping box not included in the grouping key") - } - }).collect_vec(); - - input = SR::Reduce { - input: Box::new(input), - group_key, - aggregates, - monotonic: false, - expected_group_size: None, - }; - - Ok(input.project(projection)) - } - } - BoxType::OuterJoin(box_struct) => { - // TODO: Correlated outer joins are not yet supported. - let correlation_info = the_box.correlation_info(); - if !correlation_info.is_empty() { - let msg = String::from("correlated joins are not supported yet"); - return Err(QGMError::from(UnsupportedDecorrelation { msg })); - } - - let ot = get_outer.typ(); - let oa = ot.arity(); - - // An OuterJoin box is similar to a Select box in that the - // operations join, filter, project happen in that order. - // However, between the filter and project stage, we union in - // the rows that should be preserved. - - // 0) Lower the lhs and rhs. - let mut q_iter = the_box.quantifiers.iter(); - let lhs_id = *q_iter.next().unwrap(); - let rhs_id = *q_iter.next().unwrap(); - let lhs = self.lower_quantifier(lhs_id, get_outer.clone(), outer_column_map)?; - let (lhs, _) = self.push_let(lhs); - let lt = lhs.typ().column_types.into_iter().skip(oa).collect_vec(); - let la = lt.len(); - let rhs = self.lower_quantifier(rhs_id, get_outer, outer_column_map)?; - let (rhs, _) = self.push_let(rhs); - let rt = rhs.typ().column_types.into_iter().skip(oa).collect_vec(); - let ra = rt.len(); - - // 1) Lower the inner join - let (mut inner_join, column_map) = self.lower_join_inner( - outer_column_map, - oa, - vec![(lhs_id, lhs.clone()), (rhs_id, rhs.clone())], - ); - - // 2) Lower the predicates as a filter following the - // join. - let lowered_predicates: Vec<_> = box_struct - .predicates - .iter() - .map(|p| Self::lower_expression(p, &column_map)) - .try_collect()?; - let equijoin_keys = crate::plan::lowering::derive_equijoin_cols( - oa, - la, - ra, - lowered_predicates.clone(), - ); - inner_join = inner_join.filter(lowered_predicates.into_iter()); - - let (inner_join, _) = self.push_let(inner_join); - - // 3) Calculate preserved rows - let mut result = inner_join.clone(); - let result = if let Some((l_keys, r_keys)) = equijoin_keys { - // Equijoin case - - // A collection of keys present in both left and right collections. - let (both_keys, _) = self.push_let( - inner_join - .project((0..oa).chain(l_keys.clone()).collect::>()) - .distinct(), - ); - if self.model.get_quantifier(lhs_id).quantifier_type - == QuantifierType::PRESERVED_FOREACH - { - // Rows in `left` that are matched in the inner equijoin. - let left_present = mz_expr::MirRelationExpr::join( - vec![lhs.clone(), both_keys.clone()], - (0..oa) - .chain(l_keys) - .enumerate() - .map(|(i, c)| vec![(0, c), (1, i)]) - .collect::>(), - ) - .project((0..(oa + la)).collect()); - - // Rows in `left` that are not matched in the inner equijoin. - let left_absent = left_present.negate().union(lhs); - - // Determine the types of nulls to use as filler. - let right_fill = rt - .into_iter() - .map(|typ| mz_expr::MirScalarExpr::literal_null(typ.scalar_type)) - .collect(); - - // Right-fill all left-absent elements with nulls and add them to the result. - result = left_absent.map(right_fill).union(result); - } - - if self.model.get_quantifier(rhs_id).quantifier_type - == QuantifierType::PRESERVED_FOREACH - { - // Rows in `right` that are matched in the inner equijoin. - let right_present = mz_expr::MirRelationExpr::join( - vec![rhs.clone(), both_keys], - (0..oa) - .chain(r_keys) - .enumerate() - .map(|(i, c)| vec![(0, c), (1, i)]) - .collect::>(), - ) - .project((0..(oa + ra)).collect()); - - // Rows in `left` that are not matched in the inner equijoin. - let right_absent = right_present.negate().union(rhs); - - // Determine the types of nulls to use as filler. - let left_fill = lt - .into_iter() - .map(|typ| mz_expr::MirScalarExpr::literal_null(typ.scalar_type)) - .collect(); - - // Left-fill all right-absent elements with nulls and add them to the result. - result = right_absent - .map(left_fill) - // Permute left fill before right values. - .project( - (0..oa) - .chain(oa + ra..oa + ra + la) - .chain(oa..oa + ra) - .collect(), - ) - .union(result) - } - - result - } else { - // General join case - if self.model.get_quantifier(lhs_id).quantifier_type - == QuantifierType::PRESERVED_FOREACH - { - let left_outer = lhs.anti_lookup( - &mut self.id_gen, - inner_join.clone(), - rt.into_iter() - .map(|typ| (Datum::Null, typ.scalar_type)) - .collect(), - ); - result = result.union(left_outer); - } - if self.model.get_quantifier(rhs_id).quantifier_type - == QuantifierType::PRESERVED_FOREACH - { - let right_outer = rhs - .anti_lookup( - &mut self.id_gen, - inner_join - // need to swap left and right to make the anti_lookup work - .project( - (0..oa) - .chain((oa + la)..(oa + la + ra)) - .chain((oa)..(oa + la)) - .collect(), - ), - lt.into_iter() - .map(|typ| (Datum::Null, typ.scalar_type)) - .collect(), - ) - // swap left and right back again - .project( - (0..oa) - .chain((oa + ra)..(oa + ra + la)) - .chain((oa)..(oa + ra)) - .collect(), - ); - result = result.union(right_outer); - } - result - }; - - // 4) Lower the project component. - Self::lower_box_columns(result, &the_box, &column_map, oa, oa + la + ra) - } - other => { - return Err(QGMError::from(UnsupportedBoxType { - box_type: other.clone(), - explanation: None, - })); - } - }?; - - let input = if the_box.distinct == DistinctOperation::Enforce { - input.distinct() - } else { - input - }; - let input = if the_box.ranging_quantifiers.iter().count() > 1 { - let (result, let_stack_pos) = self.push_let(input); - if let Some(let_stack_pos) = let_stack_pos { - self.common_boxes.insert(box_id, let_stack_pos); - } - result - } else { - input - }; - Ok(input) - } - - /// Generates a join among the result of applying the outer relation to the given - /// quantifiers. Returns a relation and a map of column references that can be - /// used to lower expressions that sit directly on top of the join. - /// - /// The quantifiers are joined on the columns of the outer relation. - /// TODO(asenac) Since decorrelation is not yet supported the outer relation is - /// currently always the join identity, so the result of this method is always - /// a cross-join of the given quantifiers, which makes part of this code untesteable - /// at the moment. - fn lower_join( - &mut self, - get_outer: mz_expr::MirRelationExpr, - outer_column_map: &ColumnMap, - quantifiers: &QuantifierSet, - ) -> Result<(mz_expr::MirRelationExpr, ColumnMap), QGMError> { - if let mz_expr::MirRelationExpr::Get { .. } = &get_outer { - } else { - let msg = format!( - "get_outer: expected a MirRelationExpr::Get, found {:?}", - get_outer - ); - return Err(QGMError::from(UnsupportedDecorrelation { msg })); - } - - let outer_arity = get_outer.arity(); - - let inputs = quantifiers - .iter() - .map(|q_id| { - self.lower_quantifier(*q_id, get_outer.clone(), outer_column_map) - .map(|input| (*q_id, input)) - }) - .try_collect()?; - - Ok(self.lower_join_inner(outer_column_map, outer_arity, inputs)) - } - - /// Same as `lower_join` except the outer relation has already been applied - /// to the quantifiers. - fn lower_join_inner( - &mut self, - outer_column_map: &ColumnMap, - outer_arity: usize, - inputs: Vec<(QuantifierId, mz_expr::MirRelationExpr)>, - ) -> (mz_expr::MirRelationExpr, ColumnMap) { - let (quantifiers, join_inputs): (Vec<_>, Vec<_>) = inputs.into_iter().unzip(); - let (join, input_mapper) = if join_inputs.len() == 1 { - let only_input = join_inputs.into_iter().next().unwrap(); - let input_mapper = mz_expr::JoinInputMapper::new_from_input_arities( - [outer_arity, only_input.arity() - outer_arity].into_iter(), - ); - (only_input, input_mapper) - } else { - Self::join_on_prefix(join_inputs, outer_arity) - }; - - // Generate a column map with the projection of the join plus the - // columns from the outer context. - let mut column_map = outer_column_map.clone(); - for (index, q_id) in quantifiers.iter().enumerate() { - for c in input_mapper.global_columns(index + 1) { - column_map.insert( - ColumnReference { - quantifier_id: *q_id, - position: input_mapper.map_column_to_local(c).0, - }, - c, - ); - } - } - - (join, column_map) - } - - /// Lowers the given quantifier by applying it to the outer relation. - fn lower_quantifier( - &mut self, - quantifier_id: QuantifierId, - get_outer: mz_expr::MirRelationExpr, - outer_column_map: &ColumnMap, - ) -> Result { - let quantifier = self.model.get_quantifier(quantifier_id); - let input_box = quantifier.input_box; - let mut input = self.apply(input_box, get_outer.clone(), outer_column_map)?; - - match quantifier.quantifier_type { - QuantifierType::FOREACH | QuantifierType::PRESERVED_FOREACH => { - // No special handling required - } - QuantifierType::SCALAR => { - // Add the machinery to ensure the lowered plan always produce one row, - // but one row at most, per outer key. - let col_type = input.typ().column_types.into_last(); - - let outer_arity = get_outer.arity(); - // We must determine a count for each `get_outer` prefix, - // and report an error if that count exceeds one. - let guarded = input.let_in(&mut self.id_gen, |_id_gen, get_select| { - // Count for each `get_outer` prefix. - let counts = get_select.clone().reduce( - (0..outer_arity).collect::>(), - vec![mz_expr::AggregateExpr { - func: mz_expr::AggregateFunc::Count, - expr: mz_expr::MirScalarExpr::literal_ok(Datum::True, ScalarType::Bool), - distinct: false, - }], - None, - ); - // Errors should result from counts > 1. - let errors = counts - .filter(vec![mz_expr::MirScalarExpr::Column(outer_arity) - .call_binary( - mz_expr::MirScalarExpr::literal_ok( - Datum::Int64(1), - ScalarType::Int64, - ), - mz_expr::BinaryFunc::Gt, - )]) - .project((0..outer_arity).collect::>()) - .map_one(mz_expr::MirScalarExpr::literal( - Err(mz_expr::EvalError::MultipleRowsFromSubquery), - col_type.clone().scalar_type, - )); - // Return `get_select` and any errors added in. - get_select.union(errors) - }); - // append Null to anything that didn't return any rows - let default = vec![(Datum::Null, col_type.scalar_type)]; - input = get_outer.lookup(&mut self.id_gen, guarded, default); - } - other => { - return Err(QGMError::from(UnsupportedQuantifierType { - quantifier_type: other.clone(), - context: "MIR conversion".to_string(), - })) - } - } - - Ok(input) - } - - fn lower_box_columns( - rel: mz_expr::MirRelationExpr, - the_box: &BoundRef<'_, QueryBox>, - column_map: &ColumnMap, - outer_arity: usize, - input_arity: usize, - ) -> Result { - if let Some(column_refs) = the_box.columns_as_refs() { - // if all columns projected by this box are references, we only need a projection - let mut outputs = Vec::::new(); - outputs.extend(0..outer_arity); - outputs.extend(column_refs.iter().map(|c| column_map.get(c).unwrap())); - Ok(rel.project(outputs)) - } else if !the_box.columns.is_empty() { - // otherwise, we need to apply a map to compute the outputs and then - // project them - let maps = the_box - .columns - .iter() - .map(|c| Self::lower_expression(&c.expr, column_map)) - .try_collect()?; - Ok(rel.map(maps).project( - (0..outer_arity) - .chain((input_arity)..(input_arity + the_box.columns.len())) - .collect_vec(), - )) - } else { - // for boxes without output columns, only project the outer part - Ok(rel.project((0..outer_arity).collect_vec())) - } - } - - /// Join the given inputs on a shared common prefix. - /// - /// TODO(asenac) Given the lack of support for decorrelation at the moment, this - /// method is always called with `prefix_length` 0, and hence, it remains untested. - fn join_on_prefix( - join_inputs: Vec, - prefix_length: usize, - ) -> (mz_expr::MirRelationExpr, mz_expr::JoinInputMapper) { - let input_mapper = mz_expr::JoinInputMapper::new(&join_inputs); - // Join on the outer columns - let equivalences = (0..prefix_length) - .map(|col| { - join_inputs - .iter() - .enumerate() - .map(|(input, _)| { - mz_expr::MirScalarExpr::Column( - input_mapper.map_column_to_global(col, input), - ) - }) - .collect_vec() - }) - .collect_vec(); - // Project only one copy of the outer columns - let projection = (0..prefix_length) - .chain( - (0..join_inputs.len()) - .map(|index| { - (prefix_length..input_mapper.input_arity(index)) - .map(|c| input_mapper.map_column_to_global(c, index)) - .collect_vec() - }) - .flatten(), - ) - .collect_vec(); - ( - mz_expr::MirRelationExpr::join_scalars(join_inputs, equivalences).project(projection), - mz_expr::JoinInputMapper::new_from_input_arities( - std::iter::once(prefix_length).chain( - (0..input_mapper.total_inputs()) - .into_iter() - .map(|i| input_mapper.input_arity(i) - prefix_length), - ), - ), - ) - } - - /// Lowers a scalar expression, resolving the column references using - /// the supplied column map. - fn lower_expression( - expr: &BoxScalarExpr, - column_map: &ColumnMap, - ) -> Result { - let result = match expr { - BoxScalarExpr::ColumnReference(c) => { - mz_expr::MirScalarExpr::Column(*column_map.get(c).unwrap()) - } - BoxScalarExpr::Literal(row, column_type) => { - mz_expr::MirScalarExpr::Literal(Ok(row.clone()), column_type.clone()) - } - BoxScalarExpr::CallUnmaterializable(func) => { - mz_expr::MirScalarExpr::CallUnmaterializable(func.clone()) - } - BoxScalarExpr::CallUnary { func, expr } => mz_expr::MirScalarExpr::CallUnary { - func: func.clone(), - expr: Box::new(Self::lower_expression(&*expr, column_map)?), - }, - BoxScalarExpr::CallBinary { func, expr1, expr2 } => { - mz_expr::MirScalarExpr::CallBinary { - func: func.clone(), - expr1: Box::new(Self::lower_expression(expr1, column_map)?), - expr2: Box::new(Self::lower_expression(expr2, column_map)?), - } - } - BoxScalarExpr::CallVariadic { func, exprs } => mz_expr::MirScalarExpr::CallVariadic { - func: func.clone(), - exprs: exprs - .into_iter() - .map(|expr| Self::lower_expression(expr, column_map)) - .try_collect()?, - }, - BoxScalarExpr::If { cond, then, els } => mz_expr::MirScalarExpr::If { - cond: Box::new(Self::lower_expression(cond, column_map)?), - then: Box::new(Self::lower_expression(then, column_map)?), - els: Box::new(Self::lower_expression(els, column_map)?), - }, - other => { - return Err(QGMError::from(UnsupportedBoxScalarExpr { - context: "MIR conversion".to_string(), - scalar: other.clone(), - explanation: None, - })) - } - }; - Ok(result) - } -} diff --git a/src/sql/src/query_model/mod.rs b/src/sql/src/query_model/mod.rs deleted file mode 100644 index 496b55bb5db3..000000000000 --- a/src/sql/src/query_model/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -mod attribute; -mod dot; -mod error; -mod hir; -mod mir; -mod model; -mod rewrite; -#[cfg(test)] -mod test; -mod validator; - -pub use error::QGMError; -pub use model::{BoxId, DistinctOperation, Model, QuantifierId}; -pub use validator::{ValidationError, ValidationResult}; diff --git a/src/sql/src/query_model/model/graph.rs b/src/sql/src/query_model/model/graph.rs deleted file mode 100644 index 3f63e5d7b397..000000000000 --- a/src/sql/src/query_model/model/graph.rs +++ /dev/null @@ -1,996 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -//! The Query Graph Model. -//! -//! The public interface consists of the following items: -//! * [`Model`] -//! * [`BoxId`] -//! * [`QuantifierId`] -//! * [`DistinctOperation`] -//! -//! All other types are crate-private. - -use std::cell::{Ref, RefCell, RefMut}; -use std::collections::BTreeSet; -use std::collections::{BTreeMap, HashMap, HashSet}; -use std::fmt; -use std::ops::{Deref, DerefMut}; - -use bitflags::bitflags; -use itertools::Itertools; - -use mz_expr::TableFunc; -use mz_ore::id_gen::Gen; -use mz_sql_parser::ast::Ident; - -use super::super::attribute::core::Attributes; -use super::scalar::*; - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct QuantifierId(pub u64); -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct BoxId(pub u64); - -impl std::fmt::Display for QuantifierId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl std::fmt::Display for BoxId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl From for QuantifierId { - fn from(value: u64) -> Self { - QuantifierId(value) - } -} - -impl From for BoxId { - fn from(value: u64) -> Self { - BoxId(value) - } -} - -pub(crate) type QuantifierSet = BTreeSet; - -/// A Query Graph Model instance represents a SQL query. -/// See [the design doc](https://github.com/MaterializeInc/materialize/blob/main/doc/developer/design/20210707_qgm_sql_high_level_representation.md) for details. -/// -/// In this representation, SQL queries are represented as a graph of operators, -/// represented as boxes, that are connected via quantifiers. A top-level box -/// represents the entry point of the query. -/// -/// Each non-leaf box has a set of quantifiers, which are the inputs of the -/// operation it represents. The quantifier adds information about how the -/// relation represented by its input box is consumed by the parent box. -#[derive(Debug, Default)] -pub struct Model { - /// The ID of the box representing the entry-point of the query. - pub(crate) top_box: BoxId, - /// All boxes in the query graph model. - boxes: HashMap>>, - /// Used for assigning unique IDs to query boxes. - box_id_gen: Gen, - /// All quantifiers in the query graph model. - quantifiers: HashMap>>, - /// Used for assigning unique IDs to quantifiers. - quantifier_id_gen: Gen, -} - -/// A mutable reference to an object of type `T` (a [`QueryBox`] or a [`Quantifier`]) -/// bound to a specific [`Model`]. -#[derive(Debug)] -pub(crate) struct BoundRef<'a, T> { - model: &'a Model, - r#ref: Ref<'a, T>, -} - -impl Deref for BoundRef<'_, T> { - type Target = T; - - fn deref(&self) -> &T { - self.r#ref.deref() - } -} - -/// A mutable reference to an object of type `T` (a [`QueryBox`] or a [`Quantifier`]) -/// bound to a specific [`Model`]. -#[derive(Debug)] -pub(crate) struct BoundRefMut<'a, T> { - model: &'a mut Model, - r#ref: RefMut<'a, T>, -} - -impl Deref for BoundRefMut<'_, T> { - type Target = T; - - fn deref(&self) -> &T { - self.r#ref.deref() - } -} - -impl DerefMut for BoundRefMut<'_, T> { - fn deref_mut(&mut self) -> &mut T { - self.r#ref.deref_mut() - } -} - -/// A semantic operator within a Query Graph. -#[derive(Debug)] -pub(crate) struct QueryBox { - /// uniquely identifies the box within the model - pub id: BoxId, - /// the type of the box - pub box_type: BoxType, - /// the projection of the box - pub columns: Vec, - /// the input quantifiers of the box - pub quantifiers: QuantifierSet, - /// quantifiers ranging over this box - pub ranging_quantifiers: QuantifierSet, - /// whether this box must enforce the uniqueness of its output, it is - /// guaranteed by structure of the box or it must preserve duplicated - /// rows from its input boxes. See [DistinctOperation]. - pub distinct: DistinctOperation, - /// Derived attributes - pub attributes: Attributes, -} - -/// A column projected by a `QueryBox`. -#[derive(Debug)] -pub(crate) struct Column { - pub expr: BoxScalarExpr, - pub alias: Option, -} - -/// Enum that describes the DISTINCT property of a `QueryBox`. -#[derive(Debug, Eq, Hash, PartialEq)] -pub enum DistinctOperation { - /// Distinctness of the output of the box must be enforced by - /// the box. - Enforce, - /// Distinctness of the output of the box is required, but - /// guaranteed by the structure of the box. - Guaranteed, - /// Distinctness of the output of the box is not required. - Preserve, -} - -#[derive(Debug)] -pub(crate) struct Quantifier { - /// uniquely identifiers the quantifier within the model - pub id: QuantifierId, - /// the type of the quantifier - pub quantifier_type: QuantifierType, - /// the input box of this quantifier - pub input_box: BoxId, - /// the box that owns this quantifier - pub parent_box: BoxId, - /// alias for name resolution purposes - pub alias: Option, -} - -bitflags! { - pub struct QuantifierType: u64 { - /// An ALL subquery. - const ALL = 0b00000001; - /// An existential subquery (IN SELECT/EXISTS/ANY). - const EXISTENTIAL = 0b00000010; - /// A regular join operand where each row from its input - /// box must be consumed by the parent box operator. - const FOREACH = 0b00000100; - /// The preserving side of an outer join. Only valid in - /// OuterJoin boxes. - const PRESERVED_FOREACH = 0b00001000; - /// A scalar subquery that produces one row at most. - const SCALAR = 0b00010000; - } -} - -#[derive(Debug, Clone)] -pub(crate) enum BoxType { - /// A table from the catalog. - Get(Get), - /// SQL's except operator - #[allow(dead_code)] - Except, - /// GROUP BY operator. - Grouping(Grouping), - /// SQL's intersect operator - #[allow(dead_code)] - Intersect, - /// OUTER JOIN operator. Contains one preserving quantifier - /// at most: exactly one for LEFT/RIGHT OUTER JOIN, none - /// for FULL OUTER JOIN. - OuterJoin(OuterJoin), - /// An operator that performs join, filter and project in - /// that order. - Select(Select), - /// The invocation of table function from the catalog. - CallTable(CallTable), - /// SQL's union operator - Union, - /// Operator that produces a set of rows, with potentially - /// correlated values. - Values(Values), -} - -#[derive(Debug, Clone)] -pub(crate) struct Get { - pub id: mz_repr::GlobalId, - pub unique_keys: Vec>, -} - -impl From for BoxType { - fn from(get: Get) -> Self { - BoxType::Get(get) - } -} - -/// The content of a Grouping box. -#[derive(Debug, Default, Clone)] -pub(crate) struct Grouping { - pub key: Vec, -} - -impl From for BoxType { - fn from(grouping: Grouping) -> Self { - BoxType::Grouping(grouping) - } -} - -/// The content of a OuterJoin box. -#[derive(Debug, Clone, Default)] -pub(crate) struct OuterJoin { - /// The predices in the ON clause of the outer join. - pub predicates: Vec, -} - -impl From for BoxType { - fn from(outer_join: OuterJoin) -> Self { - BoxType::OuterJoin(outer_join) - } -} - -/// The content of a Select box. -#[derive(Debug, Clone, Default)] -pub(crate) struct Select { - /// The list of predicates applied by the box. - pub predicates: Vec, - /// An optional ORDER BY key - pub order_key: Option>, - /// An optional LIMIT clause - pub limit: Option, - /// An optional OFFSET clause - pub offset: Option, -} - -impl From