mz_adapter/coord/sequencer/inner/
create_index.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
10use std::collections::BTreeMap;
11
12use maplit::btreemap;
13use mz_catalog::memory::objects::{CatalogItem, Index};
14use mz_ore::instrument;
15use mz_repr::explain::{ExprHumanizerExt, TransientItem};
16use mz_repr::optimize::{OptimizerFeatures, OverrideFrom};
17use mz_repr::{Datum, Row};
18use mz_sql::ast::ExplainStage;
19use mz_sql::catalog::CatalogError;
20use mz_sql::names::ResolvedIds;
21use mz_sql::plan;
22use tracing::Span;
23
24use crate::command::ExecuteResponse;
25use crate::coord::sequencer::inner::return_if_err;
26use crate::coord::{
27    Coordinator, CreateIndexExplain, CreateIndexFinish, CreateIndexOptimize, CreateIndexStage,
28    ExplainContext, ExplainPlanContext, Message, PlanValidity, StageResult, Staged,
29};
30use crate::error::AdapterError;
31use crate::explain::explain_dataflow;
32use crate::explain::optimizer_trace::OptimizerTrace;
33use crate::optimize::dataflows::dataflow_import_id_bundle;
34use crate::optimize::{self, Optimize};
35use crate::session::Session;
36use crate::{AdapterNotice, ExecuteContext, TimestampProvider, catalog};
37
38impl Staged for CreateIndexStage {
39    type Ctx = ExecuteContext;
40
41    fn validity(&mut self) -> &mut PlanValidity {
42        match self {
43            Self::Optimize(stage) => &mut stage.validity,
44            Self::Finish(stage) => &mut stage.validity,
45            Self::Explain(stage) => &mut stage.validity,
46        }
47    }
48
49    async fn stage(
50        self,
51        coord: &mut Coordinator,
52        ctx: &mut ExecuteContext,
53    ) -> Result<StageResult<Box<Self>>, AdapterError> {
54        match self {
55            CreateIndexStage::Optimize(stage) => coord.create_index_optimize(stage).await,
56            CreateIndexStage::Finish(stage) => {
57                coord.create_index_finish(ctx.session(), stage).await
58            }
59            CreateIndexStage::Explain(stage) => {
60                coord.create_index_explain(ctx.session(), stage).await
61            }
62        }
63    }
64
65    fn message(self, ctx: ExecuteContext, span: Span) -> Message {
66        Message::CreateIndexStageReady {
67            ctx,
68            span,
69            stage: self,
70        }
71    }
72
73    fn cancel_enabled(&self) -> bool {
74        true
75    }
76}
77
78impl Coordinator {
79    #[instrument]
80    pub(crate) async fn sequence_create_index(
81        &mut self,
82        ctx: ExecuteContext,
83        plan: plan::CreateIndexPlan,
84        resolved_ids: ResolvedIds,
85    ) {
86        let stage = return_if_err!(
87            self.create_index_validate(plan, resolved_ids, ExplainContext::None),
88            ctx
89        );
90        self.sequence_staged(ctx, Span::current(), stage).await;
91    }
92
93    #[instrument]
94    pub(crate) async fn explain_create_index(
95        &mut self,
96        ctx: ExecuteContext,
97        plan::ExplainPlanPlan {
98            stage,
99            format,
100            config,
101            explainee,
102        }: plan::ExplainPlanPlan,
103    ) {
104        let plan::Explainee::Statement(stmt) = explainee else {
105            // This is currently asserted in the `sequence_explain_plan` code that
106            // calls this method.
107            unreachable!()
108        };
109        let plan::ExplaineeStatement::CreateIndex { broken, plan } = stmt else {
110            // This is currently asserted in the `sequence_explain_plan` code that
111            // calls this method.
112            unreachable!()
113        };
114
115        // Create an OptimizerTrace instance to collect plans emitted when
116        // executing the optimizer pipeline.
117        let optimizer_trace = OptimizerTrace::new(stage.paths());
118
119        // Not used in the EXPLAIN path so it's OK to generate a dummy value.
120        let resolved_ids = ResolvedIds::empty();
121
122        let explain_ctx = ExplainContext::Plan(ExplainPlanContext {
123            broken,
124            config,
125            format,
126            stage,
127            replan: None,
128            desc: None,
129            optimizer_trace,
130        });
131        let stage = return_if_err!(
132            self.create_index_validate(plan, resolved_ids, explain_ctx),
133            ctx
134        );
135        self.sequence_staged(ctx, Span::current(), stage).await;
136    }
137
138    #[instrument]
139    pub(crate) async fn explain_replan_index(
140        &mut self,
141        ctx: ExecuteContext,
142        plan::ExplainPlanPlan {
143            stage,
144            format,
145            config,
146            explainee,
147        }: plan::ExplainPlanPlan,
148    ) {
149        let plan::Explainee::ReplanIndex(id) = explainee else {
150            unreachable!() // Asserted in `sequence_explain_plan`.
151        };
152        let CatalogItem::Index(index) = self.catalog().get_entry(&id).item() else {
153            unreachable!() // Asserted in `plan_explain_plan`.
154        };
155        let id = index.global_id();
156
157        let create_sql = index.create_sql.clone();
158        let plan_result = self
159            .catalog_mut()
160            .deserialize_plan_with_enable_for_item_parsing(&create_sql, true);
161        let (plan, resolved_ids) = return_if_err!(plan_result, ctx);
162
163        let plan::Plan::CreateIndex(plan) = plan else {
164            unreachable!() // We are parsing the `create_sql` of an `Index` item.
165        };
166
167        // It is safe to assume that query optimization will always succeed, so
168        // for now we statically assume `broken = false`.
169        let broken = false;
170
171        // Create an OptimizerTrace instance to collect plans emitted when
172        // executing the optimizer pipeline.
173        let optimizer_trace = OptimizerTrace::new(stage.paths());
174
175        let explain_ctx = ExplainContext::Plan(ExplainPlanContext {
176            broken,
177            config,
178            format,
179            stage,
180            replan: Some(id),
181            desc: None,
182            optimizer_trace,
183        });
184        let stage = return_if_err!(
185            self.create_index_validate(plan, resolved_ids, explain_ctx),
186            ctx
187        );
188        self.sequence_staged(ctx, Span::current(), stage).await;
189    }
190
191    #[instrument]
192    pub(crate) fn explain_index(
193        &mut self,
194        ctx: &ExecuteContext,
195        plan::ExplainPlanPlan {
196            stage,
197            format,
198            config,
199            explainee,
200        }: plan::ExplainPlanPlan,
201    ) -> Result<ExecuteResponse, AdapterError> {
202        let plan::Explainee::Index(id) = explainee else {
203            unreachable!() // Asserted in `sequence_explain_plan`.
204        };
205        let CatalogItem::Index(index) = self.catalog().get_entry(&id).item() else {
206            unreachable!() // Asserted in `plan_explain_plan`.
207        };
208
209        let Some(dataflow_metainfo) = self.catalog().try_get_dataflow_metainfo(&index.global_id())
210        else {
211            if !id.is_system() {
212                tracing::error!("cannot find dataflow metainformation for index {id} in catalog");
213            }
214            coord_bail!("cannot find dataflow metainformation for index {id} in catalog");
215        };
216
217        let target_cluster = self.catalog().get_cluster(index.cluster_id);
218
219        let features = OptimizerFeatures::from(self.catalog().system_config())
220            .override_from(&target_cluster.config.features())
221            .override_from(&config.features);
222
223        // TODO(mgree): calculate statistics (need a timestamp)
224        let cardinality_stats = BTreeMap::new();
225
226        let explain = match stage {
227            ExplainStage::GlobalPlan => {
228                let Some(plan) = self
229                    .catalog()
230                    .try_get_optimized_plan(&index.global_id())
231                    .cloned()
232                else {
233                    tracing::error!("cannot find {stage} for index {id} in catalog");
234                    coord_bail!("cannot find {stage} for index in catalog");
235                };
236
237                explain_dataflow(
238                    plan,
239                    format,
240                    &config,
241                    &features,
242                    &self.catalog().for_session(ctx.session()),
243                    cardinality_stats,
244                    Some(target_cluster.name.as_str()),
245                    dataflow_metainfo,
246                )?
247            }
248            ExplainStage::PhysicalPlan => {
249                let Some(plan) = self
250                    .catalog()
251                    .try_get_physical_plan(&index.global_id())
252                    .cloned()
253                else {
254                    tracing::error!("cannot find {stage} for index {id} in catalog");
255                    coord_bail!("cannot find {stage} for index in catalog");
256                };
257                explain_dataflow(
258                    plan,
259                    format,
260                    &config,
261                    &features,
262                    &self.catalog().for_session(ctx.session()),
263                    cardinality_stats,
264                    Some(target_cluster.name.as_str()),
265                    dataflow_metainfo,
266                )?
267            }
268            _ => {
269                coord_bail!("cannot EXPLAIN {} FOR INDEX", stage);
270            }
271        };
272
273        let row = Row::pack_slice(&[Datum::from(explain.as_str())]);
274
275        Ok(Self::send_immediate_rows(row))
276    }
277
278    // `explain_ctx` is an optional context set iff the state machine is initiated from
279    // sequencing an EXPLAIN for this statement.
280    #[instrument]
281    fn create_index_validate(
282        &mut self,
283        plan: plan::CreateIndexPlan,
284        resolved_ids: ResolvedIds,
285        explain_ctx: ExplainContext,
286    ) -> Result<CreateIndexStage, AdapterError> {
287        let validity =
288            PlanValidity::require_transient_revision(self.catalog().transient_revision());
289        Ok(CreateIndexStage::Optimize(CreateIndexOptimize {
290            validity,
291            plan,
292            resolved_ids,
293            explain_ctx,
294        }))
295    }
296
297    #[instrument]
298    async fn create_index_optimize(
299        &mut self,
300        CreateIndexOptimize {
301            validity,
302            plan,
303            resolved_ids,
304            explain_ctx,
305        }: CreateIndexOptimize,
306    ) -> Result<StageResult<Box<CreateIndexStage>>, AdapterError> {
307        let plan::CreateIndexPlan {
308            index: plan::Index { cluster_id, .. },
309            ..
310        } = &plan;
311
312        // Collect optimizer parameters.
313        let compute_instance = self
314            .instance_snapshot(*cluster_id)
315            .expect("compute instance does not exist");
316        let (item_id, global_id) = if let ExplainContext::None = explain_ctx {
317            let id_ts = self.get_catalog_write_ts().await;
318            self.catalog_mut().allocate_user_id(id_ts).await?
319        } else {
320            self.allocate_transient_id()
321        };
322
323        let optimizer_config = optimize::OptimizerConfig::from(self.catalog().system_config())
324            .override_from(&self.catalog.get_cluster(*cluster_id).config.features())
325            .override_from(&explain_ctx);
326
327        // Build an optimizer for this INDEX.
328        let mut optimizer = optimize::index::Optimizer::new(
329            self.owned_catalog(),
330            compute_instance,
331            global_id,
332            optimizer_config,
333            self.optimizer_metrics(),
334        );
335        let span = Span::current();
336        Ok(StageResult::Handle(mz_ore::task::spawn_blocking(
337            || "optimize create index",
338            move || {
339                span.in_scope(|| {
340                    let mut pipeline = || -> Result<(
341                    optimize::index::GlobalMirPlan,
342                    optimize::index::GlobalLirPlan,
343                ), AdapterError> {
344                    let _dispatch_guard = explain_ctx.dispatch_guard();
345
346                    let index_plan =
347                        optimize::index::Index::new(plan.name.clone(), plan.index.on, plan.index.keys.clone());
348
349                    // MIR ⇒ MIR optimization (global)
350                    let global_mir_plan = optimizer.catch_unwind_optimize(index_plan)?;
351                    // MIR ⇒ LIR lowering and LIR ⇒ LIR optimization (global)
352                    let global_lir_plan = optimizer.catch_unwind_optimize(global_mir_plan.clone())?;
353
354                    Ok((global_mir_plan, global_lir_plan))
355                };
356
357                    let stage = match pipeline() {
358                        Ok((global_mir_plan, global_lir_plan)) => {
359                            if let ExplainContext::Plan(explain_ctx) = explain_ctx {
360                                let (_, df_meta) = global_lir_plan.unapply();
361                                CreateIndexStage::Explain(CreateIndexExplain {
362                                    validity,
363                                    exported_index_id: global_id,
364                                    plan,
365                                    df_meta,
366                                    explain_ctx,
367                                })
368                            } else {
369                                CreateIndexStage::Finish(CreateIndexFinish {
370                                    validity,
371                                    item_id,
372                                    global_id,
373                                    plan,
374                                    resolved_ids,
375                                    global_mir_plan,
376                                    global_lir_plan,
377                                })
378                            }
379                        }
380                        // Internal optimizer errors are handled differently
381                        // depending on the caller.
382                        Err(err) => {
383                            let ExplainContext::Plan(explain_ctx) = explain_ctx else {
384                                // In `sequence_~` contexts, immediately error.
385                                return Err(err);
386                            };
387
388                            if explain_ctx.broken {
389                                // In `EXPLAIN BROKEN` contexts, just log the error
390                                // and move to the next stage with default
391                                // parameters.
392                                tracing::error!("error while handling EXPLAIN statement: {}", err);
393                                CreateIndexStage::Explain(CreateIndexExplain {
394                                    validity,
395                                    exported_index_id: global_id,
396                                    plan,
397                                    df_meta: Default::default(),
398                                    explain_ctx,
399                                })
400                            } else {
401                                // In regular `EXPLAIN` contexts, immediately error.
402                                return Err(err);
403                            }
404                        }
405                    };
406                    Ok(Box::new(stage))
407                })
408            },
409        )))
410    }
411
412    #[instrument]
413    async fn create_index_finish(
414        &mut self,
415        session: &Session,
416        CreateIndexFinish {
417            item_id,
418            global_id,
419            plan:
420                plan::CreateIndexPlan {
421                    name,
422                    index:
423                        plan::Index {
424                            create_sql,
425                            on,
426                            keys,
427                            cluster_id,
428                            compaction_window,
429                        },
430                    if_not_exists,
431                },
432            resolved_ids,
433            global_mir_plan,
434            global_lir_plan,
435            ..
436        }: CreateIndexFinish,
437    ) -> Result<StageResult<Box<CreateIndexStage>>, AdapterError> {
438        let id_bundle = dataflow_import_id_bundle(global_lir_plan.df_desc(), cluster_id);
439
440        let ops = vec![catalog::Op::CreateItem {
441            id: item_id,
442            name: name.clone(),
443            item: CatalogItem::Index(Index {
444                create_sql,
445                global_id,
446                keys: keys.into(),
447                on,
448                conn_id: None,
449                resolved_ids,
450                cluster_id,
451                is_retained_metrics_object: false,
452                custom_logical_compaction_window: compaction_window,
453            }),
454            owner_id: *self.catalog().get_entry_by_global_id(&on).owner_id(),
455        }];
456
457        // Pre-allocate a vector of transient GlobalIds for each notice.
458        let notice_ids = std::iter::repeat_with(|| self.allocate_transient_id())
459            .map(|(_item_id, global_id)| global_id)
460            .take(global_lir_plan.df_meta().optimizer_notices.len())
461            .collect::<Vec<_>>();
462
463        let transact_result = self
464            .catalog_transact_with_side_effects(Some(session), ops, |coord| async {
465                let (mut df_desc, df_meta) = global_lir_plan.unapply();
466
467                // Save plan structures.
468                coord
469                    .catalog_mut()
470                    .set_optimized_plan(global_id, global_mir_plan.df_desc().clone());
471                coord
472                    .catalog_mut()
473                    .set_physical_plan(global_id, df_desc.clone());
474
475                let notice_builtin_updates_fut = coord
476                    .process_dataflow_metainfo(df_meta, global_id, session, notice_ids)
477                    .await;
478
479                // We're putting in place read holds, such that ship_dataflow,
480                // below, which calls update_read_capabilities, can successfully
481                // do so. Otherwise, the since of dependencies might move along
482                // concurrently, pulling the rug from under us!
483                //
484                // TODO: Maybe in the future, pass those holds on to compute, to
485                // hold on to them and downgrade when possible?
486                let read_holds = coord.acquire_read_holds(&id_bundle);
487                let since = coord.least_valid_read(&read_holds);
488                df_desc.set_as_of(since);
489
490                coord
491                    .ship_dataflow_and_notice_builtin_table_updates(
492                        df_desc,
493                        cluster_id,
494                        notice_builtin_updates_fut,
495                    )
496                    .await;
497
498                // Drop read holds after the dataflow has been shipped, at which
499                // point compute will have put in its own read holds.
500                drop(read_holds);
501
502                coord.update_compute_read_policy(
503                    cluster_id,
504                    item_id,
505                    compaction_window.unwrap_or_default().into(),
506                );
507            })
508            .await;
509
510        match transact_result {
511            Ok(_) => Ok(StageResult::Response(ExecuteResponse::CreatedIndex)),
512            Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
513                kind:
514                    mz_catalog::memory::error::ErrorKind::Sql(CatalogError::ItemAlreadyExists(_, _)),
515            })) if if_not_exists => {
516                session.add_notice(AdapterNotice::ObjectAlreadyExists {
517                    name: name.item,
518                    ty: "index",
519                });
520                Ok(StageResult::Response(ExecuteResponse::CreatedIndex))
521            }
522            Err(err) => Err(err),
523        }
524    }
525
526    #[instrument]
527    async fn create_index_explain(
528        &mut self,
529        session: &Session,
530        CreateIndexExplain {
531            exported_index_id,
532            plan: plan::CreateIndexPlan { name, index, .. },
533            df_meta,
534            explain_ctx:
535                ExplainPlanContext {
536                    config,
537                    format,
538                    stage,
539                    optimizer_trace,
540                    ..
541                },
542            ..
543        }: CreateIndexExplain,
544    ) -> Result<StageResult<Box<CreateIndexStage>>, AdapterError> {
545        let session_catalog = self.catalog().for_session(session);
546        let expr_humanizer = {
547            let on_entry = self.catalog.get_entry_by_global_id(&index.on);
548            let full_name = self.catalog.resolve_full_name(&name, on_entry.conn_id());
549            let on_desc = on_entry
550                .desc(&full_name)
551                .expect("can only create indexes on items with a valid description");
552
553            let transient_items = btreemap! {
554                exported_index_id => TransientItem::new(
555                    Some(full_name.into_parts()),
556                    Some(on_desc.iter_names().map(|c| c.to_string()).collect()),
557                )
558            };
559            ExprHumanizerExt::new(transient_items, &session_catalog)
560        };
561
562        let target_cluster = self.catalog().get_cluster(index.cluster_id);
563
564        let features = OptimizerFeatures::from(self.catalog().system_config())
565            .override_from(&target_cluster.config.features())
566            .override_from(&config.features);
567
568        let rows = optimizer_trace
569            .into_rows(
570                format,
571                &config,
572                &features,
573                &expr_humanizer,
574                None,
575                Some(target_cluster),
576                df_meta,
577                stage,
578                plan::ExplaineeStatementKind::CreateIndex,
579                None,
580            )
581            .await?;
582
583        Ok(StageResult::Response(Self::send_immediate_rows(rows)))
584    }
585}