1use std::collections::{BTreeMap, BTreeSet};
17
18use mz_catalog::memory::objects::CatalogItem;
19use mz_repr::CatalogItemId;
20use mz_repr::{Diff, GlobalId, Row, Timestamp};
21use mz_sql::catalog::CatalogItemType;
22use mz_sql::plan::SubscribeOutput;
23use mz_storage_client::client::TableData;
24use smallvec::smallvec;
25use tokio::sync::mpsc;
26use tracing::Span;
27
28use crate::PeekResponseUnary;
29use crate::active_compute_sink::{ActiveComputeSink, ActiveSubscribe, ActiveSubscribeOwner};
30use crate::catalog::Catalog;
31use crate::command::WriteAttemptKind;
32use crate::coord::Coordinator;
33use crate::coord::appends::{
34 InternalWriteResponder, PendingWriteTxn, TableWriteCmd, TimestampedWriteRequest,
35 UserWriteResponder, WriteResult, WriteTarget,
36};
37use crate::error::AdapterError;
38use mz_ore::soft_panic_or_log;
39
40fn enqueue(
47 seen: &mut BTreeSet<CatalogItemId>,
48 stack: &mut Vec<CatalogItemId>,
49 id: CatalogItemId,
50 max_rw_dependencies: usize,
51) -> Result<(), AdapterError> {
52 if seen.insert(id) {
53 if seen.len() > max_rw_dependencies {
54 return Err(AdapterError::ReadThenWriteDependencyLimitExceeded {
55 max_rw_dependencies,
56 });
57 }
58 stack.push(id);
59 }
60 Ok(())
61}
62
63impl Coordinator {
64 #[allow(clippy::too_many_arguments)]
75 pub(crate) async fn handle_create_internal_subscribe(
76 &mut self,
77 df_desc: crate::optimize::LirDataflowDescription,
78 cluster_id: mz_compute_types::ComputeInstanceId,
79 replica_id: Option<mz_cluster_client::ReplicaId>,
80 depends_on: BTreeSet<GlobalId>,
81 as_of: Timestamp,
82 arity: usize,
83 sink_id: GlobalId,
84 owner: ActiveSubscribeOwner,
85 start_time: mz_ore::now::EpochMillis,
86 read_holds: crate::ReadHolds,
87 response_tx: tokio::sync::oneshot::Sender<
88 Result<mpsc::UnboundedReceiver<PeekResponseUnary>, AdapterError>,
89 >,
90 ) {
91 match &owner {
92 ActiveSubscribeOwner::Session { conn_id, .. } => {
94 if !self.active_conns.contains_key(conn_id) {
95 let _ = response_tx.send(Err(AdapterError::Canceled));
96 return;
97 }
98 }
99 ActiveSubscribeOwner::Background => {}
101 }
102
103 let (tx, rx) = mpsc::unbounded_channel();
104
105 let active_subscribe = ActiveSubscribe {
106 owner,
107 channel: tx,
108 backlog_accounting: std::sync::Arc::new(std::sync::Mutex::new(
109 crate::active_compute_sink::SubscribeBacklogAccounting::default(),
110 )),
111 max_buffered_bytes: usize::MAX,
118 emit_progress: true, as_of,
120 arity,
121 cluster_id,
122 depends_on,
123 start_time,
124 output: SubscribeOutput::Diffs,
125 internal: true, };
127 active_subscribe.initialize();
128
129 if let Err(err) = self
137 .try_ship_dataflow(df_desc, cluster_id, replica_id)
138 .await
139 {
140 let _ = response_tx.send(Err(
141 AdapterError::concurrent_dependency_drop_from_dataflow_creation_error(err),
142 ));
143 return;
144 }
145
146 self.add_active_compute_sink(sink_id, ActiveComputeSink::Subscribe(active_subscribe))
147 .await;
148
149 if response_tx.send(Ok(rx)).is_err() {
150 self.drop_internal_subscribe(sink_id).await;
157 return;
158 }
159
160 drop(read_holds);
163 }
164
165 pub(crate) fn handle_attempt_write(
172 &mut self,
173 attempt: WriteAttemptKind,
174 target_id: mz_repr::CatalogItemId,
175 target_global_id: GlobalId,
176 diffs: Vec<(Row, Diff)>,
177 result_tx: tokio::sync::oneshot::Sender<WriteResult>,
178 ) {
179 let result = InternalWriteResponder::new(result_tx);
180 match &attempt {
181 WriteAttemptKind::Session { conn_id, .. } => {
182 if !self.active_conns.contains_key(conn_id) {
183 result.send(WriteResult::Canceled);
184 return;
185 }
186 }
187 WriteAttemptKind::Background { .. } => {}
188 }
189 if self.controller.read_only() {
190 result.send(WriteResult::ReadOnly);
191 return;
192 }
193
194 let current_global_id = self
195 .catalog()
196 .try_get_entry(&target_id)
197 .map(|entry| entry.latest_global_id());
198 if current_global_id != Some(target_global_id) {
199 result.send(WriteResult::TargetChanged);
200 return;
201 }
202
203 let table_data = TableData::Rows(diffs);
204 let timestamped = match &attempt {
205 WriteAttemptKind::Session { write_ts, .. } => *write_ts,
206 WriteAttemptKind::Background { write_ts } => Some(*write_ts),
207 };
208 match timestamped {
209 Some(target_timestamp) => {
210 let request = TimestampedWriteRequest {
211 appends: vec![(target_global_id, vec![table_data])],
212 target_timestamp,
213 result,
214 span: Span::current(),
215 };
216 if self
217 .group_committer_tx
218 .send(TableWriteCmd::TimestampedWrite(request))
219 .is_err()
220 {
221 tracing::warn!("group committer task gone, dropping timestamped write");
222 }
223 }
224 None => {
228 let WriteAttemptKind::Session { conn_id, .. } = attempt else {
229 soft_panic_or_log!("background write reached the blind write path");
230 result.send(WriteResult::Indeterminate);
231 return;
232 };
233 let writes = BTreeMap::from([(target_id, smallvec![table_data])]);
234 self.pending_writes.push(PendingWriteTxn::User {
235 span: Span::current(),
236 writes,
237 responder: UserWriteResponder::Internal {
238 conn_id,
239 target: WriteTarget {
240 item_id: target_id,
241 global_id: target_global_id,
242 },
243 result,
244 },
245 });
246 self.trigger_group_commit();
247 }
248 }
249 }
250
251 pub(crate) async fn drop_internal_subscribe(&mut self, sink_id: GlobalId) {
253 let _ = self.drop_compute_sink(sink_id).await;
256 }
257}
258
259#[derive(Clone, Copy, Debug)]
261pub(crate) enum DependencyPolicy {
262 UserDml,
264 SystemReads,
266}
267
268pub(crate) fn validate_read_then_write_dependencies(
278 catalog: &Catalog,
279 ids: impl IntoIterator<Item = CatalogItemId>,
280 max_rw_dependencies: usize,
281 policy: DependencyPolicy,
282) -> Result<(), AdapterError> {
283 use CatalogItemType::*;
284 use mz_catalog::memory::objects;
285
286 let mut seen = BTreeSet::new();
290 let mut stack = Vec::new();
291 for id in ids {
292 enqueue(&mut seen, &mut stack, id, max_rw_dependencies)?;
293 }
294 while let Some(id) = stack.pop() {
295 let Some(entry) = catalog.try_get_entry(&id) else {
296 return Err(AdapterError::InvalidTableMutationSelection {
297 object_name: id.to_string(),
298 object_type: "unknown".to_string(),
299 });
300 };
301
302 if let CatalogItem::View(objects::View {
303 locally_optimized_expr: optimized_expr,
304 ..
305 })
306 | CatalogItem::MaterializedView(objects::MaterializedView {
307 locally_optimized_expr: optimized_expr,
308 ..
309 }) = entry.item()
310 {
311 if optimized_expr.contains_temporal() {
312 return Err(AdapterError::Unsupported(
313 "calls to mz_now in write statements",
314 ));
315 }
316 }
317
318 let item_type = entry.item().typ();
319 let ids_to_check = entry.item().query_dependencies();
320 let is_writable_table = matches!(
321 entry.item(),
322 CatalogItem::Table(objects::Table {
323 data_source: objects::TableDataSource::TableWrites { .. },
324 ..
325 })
326 );
327 let valid = match policy {
328 DependencyPolicy::UserDml => match item_type {
329 typ @ (Func | View | MaterializedView) => id.is_user() || matches!(typ, Func),
330 Source | Secret | Connection => false,
331 Sink | MetricSink | Index => unreachable!(),
333 Table => id.is_user() && is_writable_table,
334 Type => true,
335 },
336 DependencyPolicy::SystemReads => {
337 id.is_system()
338 && matches!(
339 item_type,
340 Func | View | MaterializedView | Source | Table | Type
341 )
342 }
343 };
344 if !valid {
345 let object_name = catalog.resolve_full_name(entry.name(), None).to_string();
346 let object_type = match item_type {
347 Source => "source",
349 Secret => "secret",
350 Connection => "connection",
351 Table => {
352 if !id.is_user() {
353 "system table"
354 } else if is_writable_table {
355 "user table"
356 } else if entry.source_export_details().is_some() {
357 "source-export table"
358 } else {
359 "source-backed table"
360 }
361 }
362 View if id.is_user() => "user view",
363 View => "system view",
364 MaterializedView if id.is_user() => "user materialized view",
365 MaterializedView => "system materialized view",
366 _ => "invalid dependency",
367 };
368 return Err(AdapterError::InvalidTableMutationSelection {
369 object_name,
370 object_type: object_type.to_string(),
371 });
372 }
373 for dep in ids_to_check {
374 enqueue(&mut seen, &mut stack, dep, max_rw_dependencies)?;
375 }
376 }
377 Ok(())
378}