Skip to main content

mz_adapter/coord/
sql.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//! Various utility methods used by the [`Coordinator`]. Ideally these are all
11//! put in more meaningfully named modules.
12
13use itertools::Itertools;
14use mz_adapter_types::connection::ConnectionId;
15use mz_ore::now::EpochMillis;
16use mz_repr::{Diff, GlobalId, SqlScalarType};
17use mz_sql::names::{Aug, ResolvedIds};
18use mz_sql::plan::{Params, StatementDesc};
19use mz_sql::session::metadata::SessionMetadata;
20use mz_sql_parser::ast::{Raw, Statement};
21
22use crate::active_compute_sink::{ActiveComputeSink, ActiveComputeSinkRetireReason};
23use crate::catalog::Catalog;
24use crate::coord::Coordinator;
25use crate::coord::appends::{BuiltinTableAppendCompletion, BuiltinTableAppendNotify};
26use crate::session::{Session, StateRevision, TransactionStatus};
27use crate::util::describe;
28use crate::{AdapterError, ExecuteContext, ExecuteResponse, metrics};
29
30impl Coordinator {
31    /// Plans a statement and returns the plan along with any resolved IDs
32    /// discovered inside SQL-implemented function bodies. The extra IDs should
33    /// only be used for the `restrict_to_user_objects` RBAC check.
34    pub(crate) fn plan_statement(
35        &self,
36        session: &Session,
37        stmt: mz_sql::ast::Statement<Aug>,
38        params: &mz_sql::plan::Params,
39        resolved_ids: &ResolvedIds,
40    ) -> Result<(mz_sql::plan::Plan, ResolvedIds), AdapterError> {
41        let pcx = session.pcx();
42        let catalog = self.catalog().for_session(session);
43        let (plan, sql_impl_ids) =
44            mz_sql::plan::plan(Some(pcx), &catalog, stmt, params, resolved_ids)?;
45        Ok((plan, sql_impl_ids))
46    }
47
48    pub(crate) fn declare(
49        &self,
50        mut ctx: ExecuteContext,
51        name: String,
52        stmt: Statement<Raw>,
53        sql: String,
54        params: Params,
55    ) {
56        let catalog = self.owned_catalog();
57        let now = self.now();
58        mz_ore::task::spawn(|| "coord::declare", async move {
59            let result =
60                Self::declare_inner(ctx.session_mut(), &catalog, name, stmt, sql, params, now)
61                    .map(|()| ExecuteResponse::DeclaredCursor);
62            ctx.retire(result);
63        });
64    }
65
66    fn declare_inner(
67        session: &mut Session,
68        catalog: &Catalog,
69        name: String,
70        stmt: Statement<Raw>,
71        sql: String,
72        params: Params,
73        now: EpochMillis,
74    ) -> Result<(), AdapterError> {
75        let param_types = params
76            .execute_types
77            .iter()
78            .map(|ty| Some(ty.clone()))
79            .collect::<Vec<_>>();
80        let desc = describe(catalog, stmt.clone(), &param_types, session)?;
81        session.retain_description(stmt.clone(), param_types, desc.clone());
82        let params = params
83            .datums
84            .into_iter()
85            .zip_eq(params.execute_types)
86            .collect();
87        let result_formats = vec![mz_pgwire_common::Format::Text; desc.arity()];
88        let logging = session.mint_logging(sql, Some(&stmt), now);
89        let state_revision = StateRevision {
90            catalog_revision: catalog.transient_revision(),
91            session_state_revision: session.state_revision(),
92        };
93        session.set_portal(
94            name,
95            desc,
96            Some(stmt),
97            logging,
98            params,
99            result_formats,
100            state_revision,
101        )?;
102        Ok(())
103    }
104
105    #[mz_ore::instrument(level = "debug")]
106    pub(crate) fn describe(
107        catalog: &Catalog,
108        session: &mut Session,
109        stmt: Option<Statement<Raw>>,
110        param_types: Vec<Option<SqlScalarType>>,
111    ) -> Result<StatementDesc, AdapterError> {
112        if let Some(stmt) = stmt {
113            let desc = describe(catalog, stmt.clone(), &param_types, session)?;
114            session.retain_description(stmt, param_types, desc.clone());
115            Ok(desc)
116        } else {
117            Ok(StatementDesc::new(None))
118        }
119    }
120
121    /// Verify a prepared statement is still valid. This will return an error if
122    /// the catalog's revision has changed and the statement now produces a
123    /// different type than its original.
124    pub(crate) fn verify_prepared_statement(
125        catalog: &Catalog,
126        session: &mut Session,
127        name: &str,
128    ) -> Result<(), AdapterError> {
129        let ps = match session.get_prepared_statement_unverified(name) {
130            Some(ps) => ps,
131            None => return Err(AdapterError::UnknownPreparedStatement(name.to_string())),
132        };
133        let current_state_revision = StateRevision {
134            catalog_revision: catalog.transient_revision(),
135            session_state_revision: session.state_revision(),
136        };
137        if ps.state_revision == current_state_revision {
138            return Ok(());
139        }
140        let stmt = ps.stmt().cloned();
141        let desc = ps.desc().clone();
142        let revision = ps.state_revision;
143        if let Some(new_revision) =
144            Self::verify_statement_revision(catalog, session, stmt.as_ref(), &desc, revision)?
145        {
146            let ps = session
147                .get_prepared_statement_mut_unverified(name)
148                .expect("known to exist");
149            ps.state_revision = new_revision;
150        }
151
152        Ok(())
153    }
154
155    /// Verify a portal is still valid.
156    pub(crate) fn verify_portal(
157        catalog: &Catalog,
158        session: &mut Session,
159        name: &str,
160    ) -> Result<(), AdapterError> {
161        let portal = match session.get_portal_unverified(name) {
162            Some(portal) => portal,
163            None => return Err(AdapterError::UnknownCursor(name.to_string())),
164        };
165        let current_state_revision = StateRevision {
166            catalog_revision: catalog.transient_revision(),
167            session_state_revision: session.state_revision(),
168        };
169        if portal.state_revision == current_state_revision {
170            return Ok(());
171        }
172        let stmt = portal.stmt.as_deref().cloned();
173        let desc = portal.desc.clone();
174        let revision = portal.state_revision;
175        if let Some(new_revision) =
176            Self::verify_statement_revision(catalog, session, stmt.as_ref(), &desc, revision)?
177        {
178            let portal = session
179                .get_portal_unverified_mut(name)
180                .expect("known to exist");
181            *portal.state_revision = new_revision;
182        }
183        Ok(())
184    }
185
186    /// If the current catalog/session revisions don't match the given revisions, re-describe the
187    /// statement and ensure its result type has not changed. Return `Some((c, s))` with the new
188    /// (valid) catalog and session state revisions if its plan has changed. Return `None` if the
189    /// revisions match. Return an error if the plan has changed.
190    fn verify_statement_revision(
191        catalog: &Catalog,
192        session: &mut Session,
193        stmt: Option<&Statement<Raw>>,
194        desc: &StatementDesc,
195        old_state_revision: StateRevision,
196    ) -> Result<Option<StateRevision>, AdapterError> {
197        let current_state_revision = StateRevision {
198            catalog_revision: catalog.transient_revision(),
199            session_state_revision: session.state_revision(),
200        };
201        if old_state_revision != current_state_revision {
202            let current_desc = Self::describe(
203                catalog,
204                session,
205                stmt.cloned(),
206                desc.param_types.iter().cloned().map(Some).collect(),
207            )?;
208            if &current_desc != desc {
209                Err(AdapterError::ChangedPlan(
210                    "cached plan must not change result type".to_string(),
211                ))
212            } else {
213                Ok(Some(current_state_revision))
214            }
215        } else {
216            Ok(None)
217        }
218    }
219
220    /// Handle removing in-progress transaction state regardless of the end action
221    /// of the transaction.
222    ///
223    /// Returns a notify that resolves once any `mz_subscriptions` retractions
224    /// caused by cleanup are durable.
225    pub(crate) async fn clear_transaction(
226        &mut self,
227        session: &mut Session,
228    ) -> (TransactionStatus, BuiltinTableAppendCompletion) {
229        // This function is *usually* called when transactions end, but it can fail to be called in
230        // some cases (for example if the session's role id was dropped, then we return early and
231        // don't go through the normal sequence_end_transaction path). The `Command::Commit` handler
232        // and `SessionClient::end_transaction` protect against this by each executing their parts
233        // of this function. Keep session cleanup in sync with those paths and frontend transaction
234        // completion. New coordinator resources also require transferring completion ownership
235        // through `Session::require_coordinator_completion`.
236        let retire_notify = self.clear_connection(session.conn_id()).await;
237        (session.clear_transaction(), retire_notify)
238    }
239
240    /// Clears coordinator state for a connection.
241    ///
242    /// Returns a notify that resolves once any `mz_subscriptions` retractions
243    /// caused by cleanup are durable.
244    pub(crate) async fn clear_connection(
245        &mut self,
246        conn_id: &ConnectionId,
247    ) -> BuiltinTableAppendCompletion {
248        self.connection_cancel_watches.remove(conn_id);
249        let retire_notify = self
250            .retire_compute_sinks_for_conn(conn_id, ActiveComputeSinkRetireReason::Finished)
251            .await;
252
253        // Release this transaction's compaction hold on collections.
254        if let Some(txn_reads) = self.txn_read_holds.remove(conn_id) {
255            tracing::debug!(?txn_reads, "releasing txn read holds");
256
257            // Make it explicit that we're dropping these read holds. Dropping
258            // them will release them at the Coordinator.
259            drop(txn_reads);
260        }
261
262        if let Some(guard) = self
263            .active_conns
264            .get_mut(conn_id)
265            .expect("must exist for active session")
266            .deferred_lock
267            .take()
268        {
269            self.release_ddl_lock(guard);
270        }
271
272        retire_notify
273    }
274
275    /// Adds coordinator bookkeeping for an active compute sink.
276    ///
277    /// This is a low-level method. The caller is responsible for installing the
278    /// sink in the controller.
279    pub(crate) fn add_active_compute_sink(
280        &mut self,
281        id: GlobalId,
282        active_sink: ActiveComputeSink,
283    ) -> BuiltinTableAppendNotify {
284        let session_type = match active_sink.connection_id() {
285            Some(conn_id) => {
286                let session_type =
287                    metrics::session_type_label_value(self.active_conns()[conn_id].user());
288                self.active_conns
289                    .get_mut(conn_id)
290                    .expect("must exist for active sessions")
291                    .drop_sinks
292                    .insert(id);
293                session_type
294            }
295            None => "system",
296        };
297
298        let ret_fut: BuiltinTableAppendNotify = match &active_sink {
299            ActiveComputeSink::Subscribe(active_subscribe) => {
300                match active_subscribe.introspection_session_uuid() {
301                    // An internal subscribe writes no `mz_subscriptions` row, so
302                    // it stays out of the public `mz_active_subscribes` gauge
303                    // too. Counting it there would report subscribes that
304                    // introspection deliberately shows nothing of. It gets its
305                    // own internal gauge instead, since it is still a dataflow
306                    // holding cluster resources.
307                    None => {
308                        self.metrics
309                            .active_internal_subscribes
310                            .with_label_values(&[session_type])
311                            .inc();
312
313                        Box::pin(std::future::ready(()))
314                    }
315                    Some(session_uuid) => {
316                        let update = self.catalog().state().pack_subscribe_update(
317                            id,
318                            active_subscribe,
319                            session_uuid,
320                            Diff::ONE,
321                        );
322                        let update = self.catalog().state().resolve_builtin_table_update(update);
323
324                        self.metrics
325                            .active_subscribes
326                            .with_label_values(&[session_type])
327                            .inc();
328
329                        // Defer the introspection-row write to a group commit instead of
330                        // committing it inline. An inline `execute` would block the coordinator
331                        // loop on a timestamp-oracle round trip and stall every other session.
332                        // `implement_subscribe` waits for this write before returning the
333                        // `SUBSCRIBE` response to the subscribing session.
334                        self.builtin_table_update().defer(vec![update])
335                    }
336                }
337            }
338            ActiveComputeSink::CopyTo(_) => {
339                self.metrics
340                    .active_copy_tos
341                    .with_label_values(&[session_type])
342                    .inc();
343                Box::pin(std::future::ready(()))
344            }
345        };
346        self.active_compute_sinks.insert(id, active_sink);
347        ret_fut
348    }
349
350    /// Removes coordinator bookkeeping for an active compute sink.
351    ///
352    /// Returns the removed sink together with a notify that resolves once the
353    /// `mz_subscriptions` retraction is durable. The retraction is deferred to a group
354    /// commit rather than committed inline, which would block the coordinator loop on a
355    /// timestamp-oracle round trip. Callers that expose completion of the retirement
356    /// should wait on the notify off the coordinator loop before responding. The notify is
357    /// already
358    /// resolved for sinks that write no introspection row (internal subscribes and COPY TO).
359    ///
360    /// This is a low-level method. The caller is responsible for dropping the
361    /// sink from the controller. Consider calling `drop_compute_sink` or
362    /// `retire_compute_sinks` instead.
363    #[mz_ore::instrument(level = "debug")]
364    pub(crate) async fn remove_active_compute_sink(
365        &mut self,
366        id: GlobalId,
367    ) -> Option<(ActiveComputeSink, BuiltinTableAppendNotify)> {
368        if let Some(sink) = self.active_compute_sinks.remove(&id) {
369            let session_type = match sink.connection_id() {
370                Some(conn_id) => {
371                    let session_type =
372                        metrics::session_type_label_value(self.active_conns()[conn_id].user());
373                    self.active_conns
374                        .get_mut(conn_id)
375                        .expect("must exist for active compute sink")
376                        .drop_sinks
377                        .remove(&id);
378                    session_type
379                }
380                None => "system",
381            };
382
383            let write_notify: BuiltinTableAppendNotify = match &sink {
384                ActiveComputeSink::Subscribe(active_subscribe) => {
385                    match active_subscribe.introspection_session_uuid() {
386                        // No introspection row to retract, see
387                        // `add_active_compute_sink`. The internal gauge is
388                        // decremented here to stay symmetric with it.
389                        None => {
390                            self.metrics
391                                .active_internal_subscribes
392                                .with_label_values(&[session_type])
393                                .dec();
394
395                            Box::pin(std::future::ready(()))
396                        }
397                        Some(session_uuid) => {
398                            let update = self.catalog().state().pack_subscribe_update(
399                                id,
400                                active_subscribe,
401                                session_uuid,
402                                Diff::MINUS_ONE,
403                            );
404                            let update =
405                                self.catalog().state().resolve_builtin_table_update(update);
406
407                            self.metrics
408                                .active_subscribes
409                                .with_label_values(&[session_type])
410                                .dec();
411
412                            // Defer the retraction to a group commit, for the same reason we
413                            // defer the insert (see `add_active_compute_sink`): committing inline
414                            // would block the coordinator loop. Callers that expose the
415                            // retirement wait on the notify off the coordinator loop
416                            // before responding.
417                            self.builtin_table_update().defer(vec![update])
418                        }
419                    }
420                }
421                ActiveComputeSink::CopyTo(_) => {
422                    self.metrics
423                        .active_copy_tos
424                        .with_label_values(&[session_type])
425                        .dec();
426
427                    Box::pin(std::future::ready(()))
428                }
429            };
430            Some((sink, write_notify))
431        } else {
432            None
433        }
434    }
435}