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 write_locks: None,
238 responder: UserWriteResponder::Internal {
239 conn_id,
240 target: WriteTarget {
241 item_id: target_id,
242 global_id: target_global_id,
243 },
244 result,
245 },
246 });
247 self.trigger_group_commit();
248 }
249 }
250 }
251
252 pub(crate) async fn drop_internal_subscribe(&mut self, sink_id: GlobalId) {
254 let _ = self.drop_compute_sink(sink_id).await;
257 }
258}
259
260#[derive(Clone, Copy, Debug)]
262pub(crate) enum DependencyPolicy {
263 UserDml,
265 SystemReads,
267}
268
269pub(crate) fn validate_read_then_write_dependencies(
279 catalog: &Catalog,
280 ids: impl IntoIterator<Item = CatalogItemId>,
281 max_rw_dependencies: usize,
282 policy: DependencyPolicy,
283) -> Result<(), AdapterError> {
284 use CatalogItemType::*;
285 use mz_catalog::memory::objects;
286
287 let mut seen = BTreeSet::new();
291 let mut stack = Vec::new();
292 for id in ids {
293 enqueue(&mut seen, &mut stack, id, max_rw_dependencies)?;
294 }
295 while let Some(id) = stack.pop() {
296 let Some(entry) = catalog.try_get_entry(&id) else {
297 return Err(AdapterError::InvalidTableMutationSelection {
298 object_name: id.to_string(),
299 object_type: "unknown".to_string(),
300 });
301 };
302
303 if let CatalogItem::View(objects::View {
304 locally_optimized_expr: optimized_expr,
305 ..
306 })
307 | CatalogItem::MaterializedView(objects::MaterializedView {
308 locally_optimized_expr: optimized_expr,
309 ..
310 }) = entry.item()
311 {
312 if optimized_expr.contains_temporal() {
313 return Err(AdapterError::Unsupported(
314 "calls to mz_now in write statements",
315 ));
316 }
317 }
318
319 let item_type = entry.item().typ();
320 let ids_to_check = entry.item().query_dependencies();
321 let is_writable_table = matches!(
322 entry.item(),
323 CatalogItem::Table(objects::Table {
324 data_source: objects::TableDataSource::TableWrites { .. },
325 ..
326 })
327 );
328 let valid = match policy {
329 DependencyPolicy::UserDml => match item_type {
330 typ @ (Func | View | MaterializedView) => id.is_user() || matches!(typ, Func),
331 Source | Secret | Connection => false,
332 Sink | MetricSink | Index => unreachable!(),
334 Table => id.is_user() && is_writable_table,
335 Type => true,
336 },
337 DependencyPolicy::SystemReads => {
338 id.is_system()
339 && matches!(
340 item_type,
341 Func | View | MaterializedView | Source | Table | Type
342 )
343 }
344 };
345 if !valid {
346 let object_name = catalog.resolve_full_name(entry.name(), None).to_string();
347 let object_type = match item_type {
348 Source => "source",
350 Secret => "secret",
351 Connection => "connection",
352 Table => {
353 if !id.is_user() {
354 "system table"
355 } else if is_writable_table {
356 "user table"
357 } else if entry.source_export_details().is_some() {
358 "source-export table"
359 } else {
360 "source-backed table"
361 }
362 }
363 View if id.is_user() => "user view",
364 View => "system view",
365 MaterializedView if id.is_user() => "user materialized view",
366 MaterializedView => "system materialized view",
367 _ => "invalid dependency",
368 };
369 return Err(AdapterError::InvalidTableMutationSelection {
370 object_name,
371 object_type: object_type.to_string(),
372 });
373 }
374 for dep in ids_to_check {
375 enqueue(&mut seen, &mut stack, dep, max_rw_dependencies)?;
376 }
377 }
378 Ok(())
379}