Skip to main content

mz_adapter/catalog/command/
source_statement.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;
11use std::sync::Arc;
12
13use itertools::Itertools;
14use mz_catalog::memory::objects::{CatalogItem, Source, Table};
15use mz_repr::{CatalogItemId, GlobalId};
16use mz_sql::ast::{Raw, Statement};
17use mz_sql::names::{Aug, QualifiedItemName, ResolvedIds};
18use mz_sql::plan::{Params, Plan, PlanNotice};
19use mz_sql::pure::{
20    PurifiedStatement, SourcePreparation, SourcePreparationRequest, SourcePreparationResponse,
21    SourcePreparationStatus,
22};
23use mz_sql::session::metadata::SessionMetadata;
24use tokio::sync::mpsc;
25
26use super::source::{SourceObject, SourcePlanning, plan_create_source};
27use crate::catalog::{CatalogState, ConnCatalog, Op};
28use crate::session::SessionMeta;
29use crate::{AdapterError, AdapterNotice};
30
31#[derive(Clone, Debug)]
32pub(crate) struct SourceStatement {
33    statement: Arc<Statement<Raw>>,
34    params: Params,
35    session: Arc<SessionMeta>,
36    preparation: SourcePreparation,
37    ids: BTreeMap<SourceObject, (CatalogItemId, GlobalId)>,
38}
39
40#[derive(Debug)]
41pub(crate) enum SourceResolution {
42    Prepare(SourcePreparationRequest),
43    Allocate(Vec<SourceObject>),
44    Ready(Box<SourceCompilation>),
45}
46
47#[derive(Debug)]
48pub(crate) struct SourceCompilation {
49    pub plan: Plan,
50    pub(in crate::catalog) ops: Vec<Op>,
51    pub(in crate::catalog) if_not_exists: BTreeMap<CatalogItemId, QualifiedItemName>,
52    pub(in crate::catalog) notices: Vec<PlanNotice>,
53}
54
55impl SourceStatement {
56    pub(crate) fn new(
57        statement: Arc<Statement<Raw>>,
58        params: Params,
59        session: SessionMeta,
60        now: u64,
61    ) -> Self {
62        Self {
63            statement,
64            params,
65            session: Arc::new(session),
66            preparation: SourcePreparation::new(now),
67            ids: BTreeMap::new(),
68        }
69    }
70
71    pub(in crate::catalog) fn connection_id(&self) -> &mz_adapter_types::connection::ConnectionId {
72        self.session.conn_id()
73    }
74
75    pub(crate) fn record_ids(
76        &mut self,
77        keys: Vec<SourceObject>,
78        ids: Vec<(CatalogItemId, GlobalId)>,
79    ) {
80        for (key, id) in keys.into_iter().zip_eq(ids) {
81            self.ids.entry(key).or_insert(id);
82        }
83    }
84
85    pub(crate) fn resolve(&self, state: &CatalogState) -> Result<SourceResolution, AdapterError> {
86        let (tx, rx) = mpsc::unbounded_channel();
87        let (catalog, statement) = self.input(state, tx)?;
88        let status = self.preparation.resolve(&catalog, statement)?;
89        self.compile(&catalog, status, rx)
90    }
91
92    pub(crate) fn finish(
93        &mut self,
94        state: &CatalogState,
95        response: SourcePreparationResponse,
96    ) -> Result<SourceResolution, AdapterError> {
97        let (tx, rx) = mpsc::unbounded_channel();
98        let (catalog, statement) = self.input(state, tx)?;
99        let status = self.preparation.finish(&catalog, statement, response)?;
100        self.compile(&catalog, status, rx)
101    }
102
103    fn input<'a>(
104        &self,
105        state: &'a CatalogState,
106        notices: mpsc::UnboundedSender<AdapterNotice>,
107    ) -> Result<(ConnCatalog<'a>, Statement<Aug>), AdapterError> {
108        let catalog = state.for_session_metadata(self.session.as_ref(), notices);
109        let (statement, mut resolved_ids) =
110            mz_sql::names::resolve(&catalog, (*self.statement).clone())?;
111        if matches!(&statement, Statement::AlterSource(stmt)
112            if matches!(stmt.action, mz_sql::ast::AlterSourceAction::AddSubsources { .. }))
113        {
114            if let Some(item) = mz_sql::pure::statement_source(&catalog, &statement) {
115                if let Some(source) = state.get_entry(&item.id()).source() {
116                    resolved_ids.extend_from(&source.resolved_ids);
117                }
118            }
119        }
120        mz_sql::rbac::check_purification(
121            &catalog,
122            self.session.as_ref(),
123            mz_sql::pure::statement_source(&catalog, &statement),
124            &resolved_ids,
125        )?;
126        Ok((catalog, statement))
127    }
128
129    fn compile(
130        &self,
131        catalog: &ConnCatalog<'_>,
132        status: SourcePreparationStatus,
133        mut notices_rx: mpsc::UnboundedReceiver<AdapterNotice>,
134    ) -> Result<SourceResolution, AdapterError> {
135        let purified = match status {
136            SourcePreparationStatus::Pending(request) => {
137                return Ok(SourceResolution::Prepare(request));
138            }
139            SourcePreparationStatus::Ready(statement) => *statement,
140        };
141        let (plan, mut resolved_ids, sql_impl_ids) = match purified {
142            source @ PurifiedStatement::PurifiedCreateSource { .. } => {
143                match plan_create_source(
144                    catalog,
145                    self.session.pcx(),
146                    &self.params,
147                    source,
148                    &self.ids,
149                )? {
150                    SourcePlanning::NeedsIds(keys) => {
151                        return Ok(SourceResolution::Allocate(keys));
152                    }
153                    SourcePlanning::Ready(plans) => (
154                        Plan::CreateSources(plans),
155                        ResolvedIds::empty(),
156                        ResolvedIds::empty(),
157                    ),
158                }
159            }
160            PurifiedStatement::PurifiedCreateTableFromSource { stmt } => {
161                let statement = Statement::CreateTableFromSource(stmt);
162                let resolved_ids = mz_sql::names::visit_dependencies(catalog, &statement);
163                let (plan, sql_impl_ids) = mz_sql::plan::plan(
164                    Some(self.session.pcx()),
165                    catalog,
166                    statement,
167                    &self.params,
168                    &resolved_ids,
169                )?;
170                (plan, resolved_ids, sql_impl_ids)
171            }
172            source @ PurifiedStatement::PurifiedAlterSourceAddSubsources { .. } => {
173                match super::source::plan_alter_source_add_subsources(
174                    catalog,
175                    self.session.pcx(),
176                    &self.params,
177                    source,
178                    &self.ids,
179                )? {
180                    SourcePlanning::NeedsIds(keys) => return Ok(SourceResolution::Allocate(keys)),
181                    SourcePlanning::Ready(plan) => {
182                        (plan, ResolvedIds::empty(), ResolvedIds::empty())
183                    }
184                }
185            }
186            PurifiedStatement::PurifiedAlterSourceRefreshReferences {
187                source_name,
188                available_source_references,
189            } => (
190                super::source::plan_alter_source_refresh_references(
191                    catalog,
192                    source_name,
193                    available_source_references,
194                )?,
195                ResolvedIds::empty(),
196                ResolvedIds::empty(),
197            ),
198            PurifiedStatement::PurifiedAlterSource { alter_source_stmt } => {
199                let statement = Statement::AlterSource(alter_source_stmt);
200                let resolved_ids = mz_sql::names::visit_dependencies(catalog, &statement);
201                let (plan, sql_impl_ids) = mz_sql::plan::plan(
202                    Some(self.session.pcx()),
203                    catalog,
204                    statement,
205                    &self.params,
206                    &resolved_ids,
207                )?;
208                (plan, resolved_ids, sql_impl_ids)
209            }
210            _ => return Err(AdapterError::Unsupported("unsupported source command")),
211        };
212        let mut altered_source = match &plan {
213            Plan::AlterSource(mz_sql::plan::AlterSourcePlan {
214                item_id,
215                action:
216                    mz_sql::plan::AlterSourceAction::AddSubsourceExports {
217                        options,
218                        subsources,
219                    },
220                ..
221            }) => {
222                let source =
223                    super::source::alter::rebuild_source(catalog, *item_id, options.clone())?;
224                resolved_ids.extend_from(&source.resolved_ids);
225                for subsource in subsources {
226                    resolved_ids.extend_from(&subsource.resolved_ids);
227                }
228                Some(source)
229            }
230            _ => None,
231        };
232        mz_sql::rbac::check_plan(
233            catalog,
234            None,
235            self.session.as_ref(),
236            &plan,
237            None,
238            &resolved_ids,
239            &sql_impl_ids,
240        )?;
241        let owner_id = self.session.role_metadata().current_role;
242        let mut ops = Vec::new();
243        let mut if_not_exists = BTreeMap::new();
244        let creations: &[mz_sql::plan::CreateSourcePlanBundle] = match &plan {
245            Plan::CreateSources(plans) => plans,
246            Plan::AlterSource(mz_sql::plan::AlterSourcePlan {
247                item_id, action, ..
248            }) => match action {
249                mz_sql::plan::AlterSourceAction::AddSubsourceExports { subsources, .. } => {
250                    ops.push(Op::UpdateItem {
251                        id: *item_id,
252                        name: catalog.state().get_entry(item_id).name().clone(),
253                        to_item: CatalogItem::Source(
254                            altered_source.take().expect("rebuilt source"),
255                        ),
256                    });
257                    subsources
258                }
259                mz_sql::plan::AlterSourceAction::RefreshReferences { references } => {
260                    ops.push(Op::UpdateSourceReferences {
261                        source_id: *item_id,
262                        references: references.clone().into(),
263                    });
264                    &[]
265                }
266            },
267            Plan::AlterNoop(_) => &[],
268
269            Plan::CreateTable(plan) => {
270                if plan.table.temporary {
271                    return Err(AdapterError::Unsupported("temporary source table command"));
272                }
273                let Some(&(id, global_id)) = self.ids.get(&SourceObject::Primary) else {
274                    return Ok(SourceResolution::Allocate(vec![SourceObject::Primary]));
275                };
276                if plan.if_not_exists {
277                    if_not_exists.insert(id, plan.name.clone());
278                }
279                ops.push(Op::CreateItem {
280                    id,
281                    name: plan.name.clone(),
282                    item: CatalogItem::Table(Table::from_plan(
283                        plan.table.clone(),
284                        global_id,
285                        self.session.conn_id().clone(),
286                        resolved_ids,
287                    )),
288                    owner_id,
289                });
290                &[]
291            }
292            _ => return Err(AdapterError::Unsupported("unsupported source plan")),
293        };
294        for bundle in creations {
295            let mz_sql::plan::CreateSourcePlanBundle {
296                item_id,
297                global_id,
298                plan,
299                resolved_ids,
300                available_source_references,
301            } = bundle;
302            if matches!(
303                &plan.source.data_source,
304                mz_sql::plan::DataSourceDesc::Webhook { .. }
305            ) {
306                return Err(AdapterError::Unsupported("webhook source command"));
307            }
308            if plan.if_not_exists {
309                if_not_exists.insert(*item_id, plan.name.clone());
310            }
311            ops.push(Op::CreateItem {
312                id: *item_id,
313                name: plan.name.clone(),
314                item: CatalogItem::Source(Source::new(
315                    plan.clone(),
316                    *global_id,
317                    resolved_ids.clone(),
318                    None,
319                    false,
320                )),
321                owner_id,
322            });
323            if let Some(references) = available_source_references {
324                ops.push(Op::UpdateSourceReferences {
325                    source_id: *item_id,
326                    references: references.clone().into(),
327                });
328            }
329        }
330        let mut notices = Vec::new();
331        while let Ok(notice) = notices_rx.try_recv() {
332            match notice {
333                AdapterNotice::PlanNotice(notice) => notices.push(notice),
334                _ => unreachable!("planning only emits plan notices"),
335            }
336        }
337        // Quota errors take precedence over IF NOT EXISTS collision suppression.
338        catalog
339            .state()
340            .validate_resource_limits(&ops, self.connection_id())?;
341        Ok(SourceResolution::Ready(Box::new(SourceCompilation {
342            plan,
343            ops,
344            if_not_exists,
345            notices,
346        })))
347    }
348}
349
350#[derive(Clone, Debug, PartialEq)]
351pub(crate) struct SourceOutcome {
352    skipped_name: Option<String>,
353    notices: Vec<PlanNotice>,
354}
355
356#[derive(Clone, Debug)]
357pub struct SourceCommand {
358    pub(in crate::catalog) statement: SourceStatement,
359    pub(in crate::catalog) outcome: SourceOutcome,
360    pub(in crate::catalog) observed: bool,
361}
362
363impl SourceCompilation {
364    pub(in crate::catalog) fn classify_result<T>(
365        &self,
366        result: Result<T, AdapterError>,
367    ) -> Result<(Option<T>, SourceOutcome), AdapterError> {
368        match result {
369            Ok(value) => Ok((Some(value), self.created())),
370            Err(error) => match self.skipped(&error) {
371                Some(outcome) => Ok((None, outcome)),
372                None => Err(error),
373            },
374        }
375    }
376
377    pub(in crate::catalog) fn created(&self) -> SourceOutcome {
378        SourceOutcome {
379            skipped_name: None,
380            notices: self.notices.clone(),
381        }
382    }
383
384    pub(in crate::catalog) fn skipped(&self, error: &AdapterError) -> Option<SourceOutcome> {
385        let AdapterError::Catalog(mz_catalog::memory::error::Error {
386            kind:
387                mz_catalog::memory::error::ErrorKind::Sql(
388                    mz_sql::catalog::CatalogError::ItemAlreadyExists(id, _),
389                ),
390        }) = error
391        else {
392            return None;
393        };
394        self.if_not_exists.get(id).map(|name| SourceOutcome {
395            skipped_name: Some(name.item.clone()),
396            notices: self.notices.clone(),
397        })
398    }
399}
400
401impl SourceCommand {
402    pub(in crate::catalog) fn needs_collision_probe(&self, compiled: &SourceCompilation) -> bool {
403        self.skipped_name().is_some() || (!self.observed && !compiled.if_not_exists.is_empty())
404    }
405
406    /// Creates a command whose outcome is determined by final execution.
407    pub(crate) fn unobserved(statement: SourceStatement, compiled: SourceCompilation) -> Self {
408        Self {
409            statement,
410            outcome: compiled.created(),
411            observed: false,
412        }
413    }
414
415    pub(crate) fn item_ids(&self) -> impl Iterator<Item = CatalogItemId> + '_ {
416        self.statement.ids.values().map(|(id, _)| *id)
417    }
418
419    pub(crate) fn skipped_notice(&self) -> Option<AdapterNotice> {
420        self.skipped_name()
421            .map(|name| AdapterNotice::ObjectAlreadyExists {
422                name: name.to_owned(),
423                ty: match &*self.statement.statement {
424                    Statement::CreateSource(_) => "source",
425                    Statement::CreateTableFromSource(_) => "table",
426                    _ => unreachable!("source command statement"),
427                },
428            })
429    }
430
431    pub(crate) fn record_ids(
432        &mut self,
433        keys: Vec<SourceObject>,
434        ids: Vec<(CatalogItemId, GlobalId)>,
435    ) {
436        self.statement.record_ids(keys, ids);
437    }
438
439    pub(crate) fn notices(&self) -> &[PlanNotice] {
440        &self.outcome.notices
441    }
442
443    pub(crate) fn skipped_name(&self) -> Option<&str> {
444        self.outcome.skipped_name.as_deref()
445    }
446
447    pub(in crate::catalog) fn check_outcome(
448        &mut self,
449        outcome: SourceOutcome,
450    ) -> Result<(), AdapterError> {
451        if self.observed && self.outcome != outcome {
452            return Err(AdapterError::DDLTransactionOutcomeChanged);
453        }
454        self.outcome = outcome;
455        Ok(())
456    }
457}
458
459#[derive(Debug)]
460pub(crate) struct StagedSourceStatement {
461    pub command: SourceCommand,
462    pub state: CatalogState,
463    pub snapshot: mz_catalog::durable::Snapshot,
464}