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::appends::{BuiltinTableAppendCompletion, BuiltinTableAppendNotify};
25use crate::coord::{Coordinator, Message};
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        let params = params
82            .datums
83            .into_iter()
84            .zip_eq(params.execute_types)
85            .collect();
86        let result_formats = vec![mz_pgwire_common::Format::Text; desc.arity()];
87        let logging = session.mint_logging(sql, Some(&stmt), now);
88        let state_revision = StateRevision {
89            catalog_revision: catalog.transient_revision(),
90            session_state_revision: session.state_revision(),
91        };
92        session.set_portal(
93            name,
94            desc,
95            Some(stmt),
96            logging,
97            params,
98            result_formats,
99            state_revision,
100        )?;
101        Ok(())
102    }
103
104    #[mz_ore::instrument(level = "debug")]
105    pub(crate) fn describe(
106        catalog: &Catalog,
107        session: &Session,
108        stmt: Option<Statement<Raw>>,
109        param_types: Vec<Option<SqlScalarType>>,
110    ) -> Result<StatementDesc, AdapterError> {
111        if let Some(stmt) = stmt {
112            describe(catalog, stmt, &param_types, session)
113        } else {
114            Ok(StatementDesc::new(None))
115        }
116    }
117
118    /// Verify a prepared statement is still valid. This will return an error if
119    /// the catalog's revision has changed and the statement now produces a
120    /// different type than its original.
121    pub(crate) fn verify_prepared_statement(
122        catalog: &Catalog,
123        session: &mut Session,
124        name: &str,
125    ) -> Result<(), AdapterError> {
126        let ps = match session.get_prepared_statement_unverified(name) {
127            Some(ps) => ps,
128            None => return Err(AdapterError::UnknownPreparedStatement(name.to_string())),
129        };
130        if let Some(new_revision) = Self::verify_statement_revision(
131            catalog,
132            session,
133            ps.stmt(),
134            ps.desc(),
135            ps.state_revision,
136        )? {
137            let ps = session
138                .get_prepared_statement_mut_unverified(name)
139                .expect("known to exist");
140            ps.state_revision = new_revision;
141        }
142
143        Ok(())
144    }
145
146    /// Verify a portal is still valid.
147    pub(crate) fn verify_portal(
148        catalog: &Catalog,
149        session: &mut Session,
150        name: &str,
151    ) -> Result<(), AdapterError> {
152        let portal = match session.get_portal_unverified(name) {
153            Some(portal) => portal,
154            None => return Err(AdapterError::UnknownCursor(name.to_string())),
155        };
156        if let Some(new_revision) = Self::verify_statement_revision(
157            catalog,
158            session,
159            portal.stmt.as_deref(),
160            &portal.desc,
161            portal.state_revision,
162        )? {
163            let portal = session
164                .get_portal_unverified_mut(name)
165                .expect("known to exist");
166            *portal.state_revision = new_revision;
167        }
168        Ok(())
169    }
170
171    /// If the current catalog/session revisions don't match the given revisions, re-describe the
172    /// statement and ensure its result type has not changed. Return `Some((c, s))` with the new
173    /// (valid) catalog and session state revisions if its plan has changed. Return `None` if the
174    /// revisions match. Return an error if the plan has changed.
175    fn verify_statement_revision(
176        catalog: &Catalog,
177        session: &Session,
178        stmt: Option<&Statement<Raw>>,
179        desc: &StatementDesc,
180        old_state_revision: StateRevision,
181    ) -> Result<Option<StateRevision>, AdapterError> {
182        let current_state_revision = StateRevision {
183            catalog_revision: catalog.transient_revision(),
184            session_state_revision: session.state_revision(),
185        };
186        if old_state_revision != current_state_revision {
187            let current_desc = Self::describe(
188                catalog,
189                session,
190                stmt.cloned(),
191                desc.param_types.iter().map(|ty| Some(ty.clone())).collect(),
192            )?;
193            if &current_desc != desc {
194                Err(AdapterError::ChangedPlan(
195                    "cached plan must not change result type".to_string(),
196                ))
197            } else {
198                Ok(Some(current_state_revision))
199            }
200        } else {
201            Ok(None)
202        }
203    }
204
205    /// Handle removing in-progress transaction state regardless of the end action
206    /// of the transaction.
207    ///
208    /// Returns a notify that resolves once any `mz_subscriptions` retractions
209    /// caused by cleanup are durable.
210    pub(crate) async fn clear_transaction(
211        &mut self,
212        session: &mut Session,
213    ) -> (TransactionStatus, BuiltinTableAppendCompletion) {
214        // This function is *usually* called when transactions end, but it can fail to be called in
215        // some cases (for example if the session's role id was dropped, then we return early and
216        // don't go through the normal sequence_end_transaction path). The `Command::Commit` handler
217        // and `AdapterClient::end_transaction` protect against this by each executing their parts
218        // of this function. Thus, if this function changes, ensure that the changes are propogated
219        // to either of those components.
220        let retire_notify = self.clear_connection(session.conn_id()).await;
221        (session.clear_transaction(), retire_notify)
222    }
223
224    /// Clears coordinator state for a connection.
225    ///
226    /// Returns a notify that resolves once any `mz_subscriptions` retractions
227    /// caused by cleanup are durable.
228    pub(crate) async fn clear_connection(
229        &mut self,
230        conn_id: &ConnectionId,
231    ) -> BuiltinTableAppendCompletion {
232        self.connection_cancel_watches.remove(conn_id);
233        let retire_notify = self
234            .retire_compute_sinks_for_conn(conn_id, ActiveComputeSinkRetireReason::Finished)
235            .await;
236        self.retire_cluster_reconfigurations_for_conn(conn_id).await;
237
238        // Release this transaction's compaction hold on collections.
239        if let Some(txn_reads) = self.txn_read_holds.remove(conn_id) {
240            tracing::debug!(?txn_reads, "releasing txn read holds");
241
242            // Make it explicit that we're dropping these read holds. Dropping
243            // them will release them at the Coordinator.
244            drop(txn_reads);
245        }
246
247        if let Some(_guard) = self
248            .active_conns
249            .get_mut(conn_id)
250            .expect("must exist for active session")
251            .deferred_lock
252            .take()
253        {
254            // If there are waiting deferred statements, process one.
255            if !self.serialized_ddl.is_empty() {
256                let _ = self.internal_cmd_tx.send(Message::DeferredStatementReady);
257            }
258        }
259
260        retire_notify
261    }
262
263    /// Adds coordinator bookkeeping for an active compute sink.
264    ///
265    /// This is a low-level method. The caller is responsible for installing the
266    /// sink in the controller.
267    pub(crate) fn add_active_compute_sink(
268        &mut self,
269        id: GlobalId,
270        active_sink: ActiveComputeSink,
271    ) -> BuiltinTableAppendNotify {
272        let user = self.active_conns()[active_sink.connection_id()].user();
273        let session_type = metrics::session_type_label_value(user);
274
275        self.active_conns
276            .get_mut(active_sink.connection_id())
277            .expect("must exist for active sessions")
278            .drop_sinks
279            .insert(id);
280
281        let ret_fut: BuiltinTableAppendNotify = match &active_sink {
282            ActiveComputeSink::Subscribe(active_subscribe) => {
283                let table_update_fut = if !active_subscribe.internal {
284                    let update = self.catalog().state().pack_subscribe_update(
285                        id,
286                        active_subscribe,
287                        Diff::ONE,
288                    );
289                    let update = self.catalog().state().resolve_builtin_table_update(update);
290
291                    // Defer the introspection-row write to a group commit instead of
292                    // committing it inline. An inline `execute` would block the coordinator
293                    // loop on a timestamp-oracle round trip and stall every other session.
294                    // `implement_subscribe` waits for this write before returning the
295                    // `SUBSCRIBE` response to the subscribing session.
296                    self.builtin_table_update().defer(vec![update])
297                } else {
298                    // Internal subscribes skip the builtin table update.
299                    Box::pin(std::future::ready(()))
300                };
301
302                self.metrics
303                    .active_subscribes
304                    .with_label_values(&[session_type])
305                    .inc();
306
307                table_update_fut
308            }
309            ActiveComputeSink::CopyTo(_) => {
310                self.metrics
311                    .active_copy_tos
312                    .with_label_values(&[session_type])
313                    .inc();
314                Box::pin(std::future::ready(()))
315            }
316        };
317        self.active_compute_sinks.insert(id, active_sink);
318        ret_fut
319    }
320
321    /// Removes coordinator bookkeeping for an active compute sink.
322    ///
323    /// Returns the removed sink together with a notify that resolves once the
324    /// `mz_subscriptions` retraction is durable. The retraction is deferred to a group
325    /// commit rather than committed inline, which would block the coordinator loop on a
326    /// timestamp-oracle round trip. Callers that expose completion of the retirement
327    /// should wait on the notify off the loop before responding. The notify is already
328    /// resolved for sinks that write no introspection row (internal subscribes and COPY TO).
329    ///
330    /// This is a low-level method. The caller is responsible for dropping the
331    /// sink from the controller. Consider calling `drop_compute_sink` or
332    /// `retire_compute_sinks` instead.
333    #[mz_ore::instrument(level = "debug")]
334    pub(crate) async fn remove_active_compute_sink(
335        &mut self,
336        id: GlobalId,
337    ) -> Option<(ActiveComputeSink, BuiltinTableAppendNotify)> {
338        if let Some(sink) = self.active_compute_sinks.remove(&id) {
339            let user = self.active_conns()[sink.connection_id()].user();
340            let session_type = metrics::session_type_label_value(user);
341
342            self.active_conns
343                .get_mut(sink.connection_id())
344                .expect("must exist for active compute sink")
345                .drop_sinks
346                .remove(&id);
347
348            let write_notify: BuiltinTableAppendNotify = match &sink {
349                ActiveComputeSink::Subscribe(active_subscribe) => {
350                    // Internal subscribes write no introspection row.
351                    let notify = if !active_subscribe.internal {
352                        let update = self.catalog().state().pack_subscribe_update(
353                            id,
354                            active_subscribe,
355                            Diff::MINUS_ONE,
356                        );
357                        let update = self.catalog().state().resolve_builtin_table_update(update);
358                        // Defer the retraction to a group commit, for the same reason we
359                        // defer the insert (see `add_active_compute_sink`): committing inline
360                        // would block the coordinator loop. Callers that expose the
361                        // retirement wait on the notify off the loop before responding.
362                        self.builtin_table_update().defer(vec![update])
363                    } else {
364                        Box::pin(std::future::ready(()))
365                    };
366
367                    self.metrics
368                        .active_subscribes
369                        .with_label_values(&[session_type])
370                        .dec();
371
372                    notify
373                }
374                ActiveComputeSink::CopyTo(_) => {
375                    self.metrics
376                        .active_copy_tos
377                        .with_label_values(&[session_type])
378                        .dec();
379
380                    Box::pin(std::future::ready(()))
381                }
382            };
383            Some((sink, write_notify))
384        } else {
385            None
386        }
387    }
388}