Skip to main content

mz_adapter/optimize/
copy_to.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Optimizer implementation for `COPY TO` statements.
11
12use std::fmt::Debug;
13use std::sync::Arc;
14use std::time::{Duration, Instant};
15
16use mz_compute_types::plan::LirRelationExpr;
17use mz_compute_types::sinks::{
18    ComputeSinkConnection, ComputeSinkDesc, CopyToS3OneshotSinkConnection,
19};
20use mz_expr::{MirRelationExpr, OptimizedMirRelationExpr};
21use mz_repr::GlobalId;
22use mz_repr::explain::trace_plan;
23use mz_sql::optimizer_metrics::OptimizerMetrics;
24use mz_sql::plan::HirRelationExpr;
25use mz_sql::session::metadata::SessionMetadata;
26use mz_storage_types::connections::Connection;
27use mz_storage_types::sinks::S3UploadInfo;
28use mz_transform::dataflow::DataflowMetainfo;
29use mz_transform::normalize_lets::normalize_lets;
30use mz_transform::typecheck::{SharedTypecheckingContext, empty_typechecking_context};
31use mz_transform::{StatisticsOracle, TransformCtx};
32use timely::progress::Antichain;
33use tracing::warn;
34
35use crate::TimestampContext;
36use crate::catalog::Catalog;
37use crate::coord::CopyToContext;
38use crate::optimize::dataflows::{
39    ComputeInstanceSnapshot, DataflowBuilder, EvalTime, ExprPrep, ExprPrepOneShot,
40};
41use crate::optimize::{
42    LirDataflowDescription, MirDataflowDescription, Optimize, OptimizeMode, OptimizerConfig,
43    OptimizerError, optimize_mir_local, trace_plan,
44};
45
46pub struct Optimizer {
47    /// A representation typechecking context to use throughout the optimizer pipeline.
48    typecheck_ctx: SharedTypecheckingContext,
49    /// A snapshot of the catalog state.
50    catalog: Arc<Catalog>,
51    /// A snapshot of the cluster that will run the dataflows.
52    compute_instance: ComputeInstanceSnapshot,
53    /// A transient GlobalId to be used when constructing the dataflow.
54    select_id: GlobalId,
55    /// Data required to do a COPY TO query.
56    copy_to_context: CopyToContext,
57    /// Optimizer config.
58    config: OptimizerConfig,
59    /// Optimizer metrics.
60    metrics: OptimizerMetrics,
61    /// The time spent performing optimization so far.
62    duration: Duration,
63}
64
65impl Optimizer {
66    pub fn new(
67        catalog: Arc<Catalog>,
68        compute_instance: ComputeInstanceSnapshot,
69        select_id: GlobalId,
70        copy_to_context: CopyToContext,
71        config: OptimizerConfig,
72        metrics: OptimizerMetrics,
73    ) -> Self {
74        Self {
75            typecheck_ctx: empty_typechecking_context(),
76            catalog,
77            compute_instance,
78            select_id,
79            copy_to_context,
80            config,
81            metrics,
82            duration: Default::default(),
83        }
84    }
85}
86
87// A bogey `Debug` implementation that hides fields, so that debug-formatting an
88// optimizer (e.g. in a tracing event) does not emit a lot of data.
89//
90// For now, we skip almost all fields, but we might revisit that bit if it turns
91// out that we really need those for debugging purposes.
92impl Debug for Optimizer {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        f.debug_struct("OptimizePeek")
95            .field("config", &self.config)
96            .finish_non_exhaustive()
97    }
98}
99
100/// Marker type for [`LocalMirPlan`] representing an optimization result without
101/// context.
102pub struct Unresolved;
103
104/// The (sealed intermediate) result after HIR ⇒ MIR lowering and decorrelation
105/// and MIR optimization.
106#[derive(Clone)]
107pub struct LocalMirPlan<T = Unresolved> {
108    expr: MirRelationExpr,
109    df_meta: DataflowMetainfo,
110    context: T,
111}
112
113/// Marker type for [`LocalMirPlan`] structs representing an optimization result
114/// with attached environment context required for the next optimization stage.
115pub struct Resolved<'s> {
116    timestamp_ctx: TimestampContext,
117    stats: Box<dyn StatisticsOracle>,
118    session: &'s dyn SessionMetadata,
119}
120
121/// The (final) result after
122///
123/// 1. embedding a [`LocalMirPlan`] into a `DataflowDescription`,
124/// 2. transitively inlining referenced views,
125/// 3. timestamp resolution,
126/// 4. optimizing the resulting `DataflowDescription` with `MIR` plans.
127/// 5. MIR ⇒ LIR lowering, and
128/// 6. optimizing the resulting `DataflowDescription` with `LIR` plans.
129#[derive(Debug)]
130pub struct GlobalLirPlan {
131    df_desc: LirDataflowDescription,
132    df_meta: DataflowMetainfo,
133}
134
135impl GlobalLirPlan {
136    pub fn df_desc(&self) -> &LirDataflowDescription {
137        &self.df_desc
138    }
139}
140
141impl Optimize<HirRelationExpr> for Optimizer {
142    type To = LocalMirPlan;
143
144    fn optimize(&mut self, expr: HirRelationExpr) -> Result<Self::To, OptimizerError> {
145        let time = Instant::now();
146
147        // Trace the pipeline input under `optimize/raw`.
148        trace_plan!(at: "raw", &expr);
149
150        // HIR ⇒ MIR lowering and decorrelation
151        let expr = expr.lower(&self.config, Some(&self.metrics))?;
152
153        // MIR ⇒ MIR optimization (local)
154        let mut df_meta = DataflowMetainfo::default();
155        let mut transform_ctx = TransformCtx::local(
156            &self.config.features,
157            &self.typecheck_ctx,
158            &mut df_meta,
159            Some(&mut self.metrics),
160            Some(self.select_id),
161        );
162        let expr = optimize_mir_local(expr, &mut transform_ctx)?.into_inner();
163
164        self.duration += time.elapsed();
165
166        // Return the (sealed) plan at the end of this optimization step.
167        Ok(LocalMirPlan {
168            expr,
169            df_meta,
170            context: Unresolved,
171        })
172    }
173}
174
175impl LocalMirPlan<Unresolved> {
176    /// Produces the [`LocalMirPlan`] with [`Resolved`] contextual information
177    /// required for the next stage.
178    pub fn resolve(
179        self,
180        timestamp_ctx: TimestampContext,
181        session: &dyn SessionMetadata,
182        stats: Box<dyn StatisticsOracle>,
183    ) -> LocalMirPlan<Resolved<'_>> {
184        LocalMirPlan {
185            expr: self.expr,
186            df_meta: self.df_meta,
187            context: Resolved {
188                timestamp_ctx,
189                session,
190                stats,
191            },
192        }
193    }
194}
195
196impl<'s> Optimize<LocalMirPlan<Resolved<'s>>> for Optimizer {
197    type To = GlobalLirPlan;
198
199    fn optimize(&mut self, plan: LocalMirPlan<Resolved<'s>>) -> Result<Self::To, OptimizerError> {
200        let time = Instant::now();
201
202        let LocalMirPlan {
203            expr,
204            mut df_meta,
205            context:
206                Resolved {
207                    timestamp_ctx,
208                    stats,
209                    session,
210                },
211        } = plan;
212
213        let expr = OptimizedMirRelationExpr(expr);
214
215        // The assembled dataflow contains a view and a sink on that view.
216        let mut df_builder = {
217            let catalog = self.catalog.state();
218            let compute = self.compute_instance.clone();
219            DataflowBuilder::new(catalog, compute).with_config(&self.config)
220        };
221
222        let debug_name = format!("copy-to-{}", self.select_id);
223        let mut df_desc = MirDataflowDescription::new(debug_name.to_string());
224
225        df_builder.import_view_into_dataflow(
226            &self.select_id,
227            &expr,
228            &mut df_desc,
229            &self.config.features,
230        )?;
231        df_builder.maybe_reoptimize_imported_views(&mut df_desc, &self.config)?;
232
233        // Creating an S3 sink as currently only s3 sinks are supported. It
234        // might be possible in the future for COPY TO to write to different
235        // sinks, which should be set here depending upon the url scheme.
236        let connection = match &self.copy_to_context.connection {
237            Connection::Aws(aws_connection) => {
238                ComputeSinkConnection::CopyToS3Oneshot(CopyToS3OneshotSinkConnection {
239                    upload_info: S3UploadInfo {
240                        uri: self.copy_to_context.uri.to_string(),
241                        max_file_size: self.copy_to_context.max_file_size,
242                        desc: self.copy_to_context.desc.clone(),
243                        format: self.copy_to_context.format.clone(),
244                    },
245                    aws_connection: aws_connection.clone(),
246                    connection_id: self.copy_to_context.connection_id,
247                    output_batch_count: self
248                        .copy_to_context
249                        .output_batch_count
250                        .expect("output_batch_count should be set in sequencer"),
251                })
252            }
253            _ => {
254                // Currently only s3 sinks are supported. It was already validated in planning that this
255                // is an aws connection.
256                let msg = "only aws connection is supported in COPY TO";
257                return Err(OptimizerError::Internal(msg.to_string()));
258            }
259        };
260        let sink_description = ComputeSinkDesc {
261            from_desc: self.copy_to_context.desc.clone(),
262            from: self.select_id,
263            connection,
264            with_snapshot: true,
265            // This will get updated  when the GlobalMirPlan is resolved with as_of below.
266            up_to: Default::default(),
267            // No `FORCE NOT NULL` for copy_to.
268            non_null_assertions: Vec::new(),
269            // No `REFRESH` for copy_to.
270            refresh_schedule: None,
271            // Populated during LIR lowering.
272            from_arrangement: None,
273        };
274        df_desc.export_sink(self.select_id, sink_description);
275
276        // Set the `as_of` and `until` timestamps for the dataflow.
277        df_desc.set_as_of(timestamp_ctx.antichain());
278
279        // Get the single timestamp representing the `as_of` time.
280        let as_of = df_desc
281            .as_of
282            .clone()
283            .expect("as_of antichain")
284            .into_option()
285            .expect("unique as_of element");
286
287        // Resolve all unmaterializable function calls including mz_now().
288        let style = ExprPrepOneShot {
289            logical_time: EvalTime::Time(as_of),
290            session,
291            catalog_state: self.catalog.state(),
292        };
293        df_desc.visit_children(
294            |r| style.prep_relation_expr(r),
295            |s| style.prep_scalar_expr(s),
296        )?;
297
298        // Use the opportunity to name an `until` frontier that will prevent
299        // work we needn't perform. By default, `until` will be
300        // `Antichain::new()`, which prevents no updates and is safe.
301        //
302        // If `timestamp_ctx.antichain()` is empty, `timestamp_ctx.timestamp()`
303        // will return `None` and we use the default (empty) `until`. Otherwise,
304        // we expect to be able to set `until = as_of + 1` without an overflow.
305        if let Some(as_of) = timestamp_ctx.timestamp() {
306            if let Some(until) = as_of.checked_add(1) {
307                df_desc.until = Antichain::from_elem(until);
308                // Also updating the sink up_to
309                for (_, sink) in &mut df_desc.sink_exports {
310                    sink.up_to.clone_from(&df_desc.until);
311                }
312            } else {
313                warn!(as_of = %as_of, "as_of + 1 overflow");
314            }
315        }
316
317        // Construct TransformCtx for global optimization.
318        let mut transform_ctx = TransformCtx::global(
319            &df_builder,
320            &*stats,
321            &self.config.features,
322            &self.typecheck_ctx,
323            &mut df_meta,
324            Some(&mut self.metrics),
325        );
326        // Run global optimization.
327        mz_transform::optimize_dataflow(&mut df_desc, &mut transform_ctx, false)?;
328
329        if self.config.mode == OptimizeMode::Explain {
330            // Collect the list of indexes used by the dataflow at this point.
331            trace_plan!(at: "global", &df_meta.used_indexes(&df_desc));
332        }
333
334        // Ensure all expressions are normalized before finalizing.
335        for build in df_desc.objects_to_build.iter_mut() {
336            normalize_lets(&mut build.plan.0, &self.config.features)?
337        }
338
339        // Finalize the dataflow. This includes:
340        // - MIR ⇒ LIR lowering
341        // - LIR ⇒ LIR transforms
342        let df_desc = LirRelationExpr::finalize_dataflow(
343            df_desc,
344            &self.config.features,
345            Some(self.metrics.lowering()),
346        )?;
347
348        // Trace the pipeline output under `optimize`.
349        trace_plan(&df_desc);
350
351        self.duration += time.elapsed();
352        self.metrics
353            .observe_e2e_optimization_time("copy_to", self.duration);
354
355        Ok(GlobalLirPlan { df_desc, df_meta })
356    }
357}
358
359impl GlobalLirPlan {
360    /// Unwraps the parts of the final result of the optimization pipeline.
361    pub fn unapply(self) -> (LirDataflowDescription, DataflowMetainfo) {
362        (self.df_desc, self.df_meta)
363    }
364}