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                if active_subscribe.internal {
284                    // An internal subscribe writes no `mz_subscriptions` row, so
285                    // it stays out of the public `mz_active_subscribes` gauge
286                    // too. Counting it there would report subscribes that
287                    // introspection deliberately shows nothing of. It gets its
288                    // own internal gauge instead, since it is still a dataflow
289                    // holding cluster resources.
290                    self.metrics
291                        .active_internal_subscribes
292                        .with_label_values(&[session_type])
293                        .inc();
294
295                    Box::pin(std::future::ready(()))
296                } else {
297                    let update = self.catalog().state().pack_subscribe_update(
298                        id,
299                        active_subscribe,
300                        Diff::ONE,
301                    );
302                    let update = self.catalog().state().resolve_builtin_table_update(update);
303
304                    self.metrics
305                        .active_subscribes
306                        .with_label_values(&[session_type])
307                        .inc();
308
309                    // Defer the introspection-row write to a group commit instead of
310                    // committing it inline. An inline `execute` would block the coordinator
311                    // loop on a timestamp-oracle round trip and stall every other session.
312                    // `implement_subscribe` waits for this write before returning the
313                    // `SUBSCRIBE` response to the subscribing session.
314                    self.builtin_table_update().defer(vec![update])
315                }
316            }
317            ActiveComputeSink::CopyTo(_) => {
318                self.metrics
319                    .active_copy_tos
320                    .with_label_values(&[session_type])
321                    .inc();
322                Box::pin(std::future::ready(()))
323            }
324        };
325        self.active_compute_sinks.insert(id, active_sink);
326        ret_fut
327    }
328
329    /// Removes coordinator bookkeeping for an active compute sink.
330    ///
331    /// Returns the removed sink together with a notify that resolves once the
332    /// `mz_subscriptions` retraction is durable. The retraction is deferred to a group
333    /// commit rather than committed inline, which would block the coordinator loop on a
334    /// timestamp-oracle round trip. Callers that expose completion of the retirement
335    /// should wait on the notify off the coordinator loop before responding. The notify is
336    /// already
337    /// resolved for sinks that write no introspection row (internal subscribes and COPY TO).
338    ///
339    /// This is a low-level method. The caller is responsible for dropping the
340    /// sink from the controller. Consider calling `drop_compute_sink` or
341    /// `retire_compute_sinks` instead.
342    #[mz_ore::instrument(level = "debug")]
343    pub(crate) async fn remove_active_compute_sink(
344        &mut self,
345        id: GlobalId,
346    ) -> Option<(ActiveComputeSink, BuiltinTableAppendNotify)> {
347        if let Some(sink) = self.active_compute_sinks.remove(&id) {
348            let user = self.active_conns()[sink.connection_id()].user();
349            let session_type = metrics::session_type_label_value(user);
350
351            self.active_conns
352                .get_mut(sink.connection_id())
353                .expect("must exist for active compute sink")
354                .drop_sinks
355                .remove(&id);
356
357            let write_notify: BuiltinTableAppendNotify = match &sink {
358                ActiveComputeSink::Subscribe(active_subscribe) => {
359                    if active_subscribe.internal {
360                        // No introspection row to retract, see
361                        // `add_active_compute_sink`. The internal gauge is
362                        // decremented here to stay symmetric with it.
363                        self.metrics
364                            .active_internal_subscribes
365                            .with_label_values(&[session_type])
366                            .dec();
367
368                        Box::pin(std::future::ready(()))
369                    } else {
370                        let update = self.catalog().state().pack_subscribe_update(
371                            id,
372                            active_subscribe,
373                            Diff::MINUS_ONE,
374                        );
375                        let update = self.catalog().state().resolve_builtin_table_update(update);
376
377                        self.metrics
378                            .active_subscribes
379                            .with_label_values(&[session_type])
380                            .dec();
381
382                        // Defer the retraction to a group commit, for the same reason we
383                        // defer the insert (see `add_active_compute_sink`): committing inline
384                        // would block the coordinator loop. Callers that expose the
385                        // retirement wait on the notify off the coordinator loop
386                        // before responding.
387                        self.builtin_table_update().defer(vec![update])
388                    }
389                }
390                ActiveComputeSink::CopyTo(_) => {
391                    self.metrics
392                        .active_copy_tos
393                        .with_label_values(&[session_type])
394                        .dec();
395
396                    Box::pin(std::future::ready(()))
397                }
398            };
399            Some((sink, write_notify))
400        } else {
401            None
402        }
403    }
404}