1use std::collections::HashMap;
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8use arrow::array::RecordBatch;
9use datafusion::catalog::default_table_source::source_as_provider;
10use datafusion::dataframe::DataFrame;
11use datafusion::error::DataFusionError;
12use datafusion::execution::SessionState;
13use datafusion::logical_expr::{Expr, LogicalPlan};
14use datafusion::prelude::SessionContext;
15use datafusion_common::tree_node::TreeNodeRecursion;
16
17use super::append_log::LogReadError;
18use super::committed::{CommittedReadError, CommittedViews, PinnedCut, ViewShape};
19use super::{MvStore, ViewRead};
20use crate::error::DbError;
21use crate::table_provider::MvTableProvider;
22
23#[derive(Debug)]
26pub(crate) struct StatementViewSnapshot {
27 views: HashMap<String, RecordBatch>,
28}
29
30impl StatementViewSnapshot {
31 pub(crate) fn get(&self, view: &str) -> Option<&RecordBatch> {
32 self.views.get(view)
33 }
34}
35
36pub(crate) struct CommittedStatement {
39 views: Arc<CommittedViews>,
40 cut: PinnedCut,
41 shapes: HashMap<String, ViewShape>,
42}
43
44impl std::fmt::Debug for CommittedStatement {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 f.debug_struct("CommittedStatement")
47 .field("epoch", &self.cut.epoch)
48 .field("views", &self.shapes.keys())
49 .finish_non_exhaustive()
50 }
51}
52
53impl CommittedStatement {
54 pub(crate) async fn read(
55 &self,
56 view: &str,
57 filters: &[Expr],
58 ) -> Result<Vec<RecordBatch>, DbError> {
59 let shape = self.shapes.get(view).ok_or_else(|| {
60 DbError::MaterializedView(format!("view '{view}' was not planned for this read"))
61 })?;
62 self.cut.read_view(&self.views, shape, filters).await
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct ReadCut {
69 pub epoch: u64,
71 pub checkpoint_id: u64,
73 pub taken_at_ms: u64,
75}
76
77const VIEW_READ_ATTEMPTS: u32 = 4;
79const VIEW_READ_RETRY_DELAY: Duration = Duration::from_millis(20);
80pub(crate) const VIEW_READ_DEADLINE: Duration = Duration::from_secs(30);
82
83pub(crate) async fn read_views(
87 store: &parking_lot::RwLock<MvStore>,
88 views: &[String],
89) -> Result<HashMap<String, RecordBatch>, DbError> {
90 let (views, ()) = read_captured(store, &views.join(", "), |guard| {
91 let mut captured = Vec::with_capacity(views.len());
92 for view in views {
93 if let Some(read) = guard.read(view)? {
94 captured.push((view.clone(), read));
95 }
96 }
97 Ok((captured, ()))
98 })
99 .await?;
100 Ok(views)
101}
102
103pub(crate) async fn read_captured<T>(
107 store: &parking_lot::RwLock<MvStore>,
108 described: &str,
109 mut capture: impl FnMut(&MvStore) -> Result<(Vec<(String, ViewRead)>, T), DbError>,
110) -> Result<(HashMap<String, RecordBatch>, T), DbError> {
111 let mut attempt = 1;
112 loop {
113 let (captured, taken) = capture(&store.read())?;
114 match materialize(captured, Instant::now() + VIEW_READ_DEADLINE).await {
115 Ok(views) => return Ok((views, taken)),
116 Err(LogReadError::Failed(error)) => return Err(error),
117 Err(LogReadError::Retired) if attempt < VIEW_READ_ATTEMPTS => {
118 drop(taken);
119 tokio::time::sleep(VIEW_READ_RETRY_DELAY * attempt).await;
120 attempt += 1;
121 }
122 Err(LogReadError::Retired) => {
123 return Err(DbError::MaterializedView(format!(
124 "materialized views [{described}] were checkpointed {VIEW_READ_ATTEMPTS} \
125 times while being read; retry the statement"
126 )))
127 }
128 }
129 }
130}
131
132async fn materialize(
133 captured: Vec<(String, ViewRead)>,
134 deadline: Instant,
135) -> Result<HashMap<String, RecordBatch>, LogReadError> {
136 let mut views = HashMap::with_capacity(captured.len());
137 for (view, read) in captured {
138 let batch = match read {
139 ViewRead::Ready(batch) => batch,
140 ViewRead::Log(log) => log.materialize(deadline).await?,
141 };
142 views.insert(view, batch);
143 }
144 Ok(views)
145}
146
147pub(crate) async fn plan_statement(
150 ctx: &SessionContext,
151 store: &parking_lot::RwLock<MvStore>,
152 sql: &str,
153) -> Result<DataFrame, DbError> {
154 let mut state = ctx.state();
155 let plan = state.create_logical_plan(sql).await?;
156 if matches!(plan, LogicalPlan::Ddl(_) | LogicalPlan::Statement(_)) {
157 return Ok(ctx.execute_logical_plan(plan).await?);
158 }
159 let views = scanned_tables(&plan)?.views;
160 if !views.is_empty() && store.read().holds_node_part() {
161 return Err(DbError::InvalidOperation(format!(
162 "a cluster node holds only its part of materialized views [{}]; read them at a \
163 committed cut",
164 views.join(", ")
165 )));
166 }
167 if !views.is_empty() {
168 let snapshot = StatementViewSnapshot {
169 views: read_views(store, &views).await?,
170 };
171 state
172 .config_mut()
173 .set_extension(std::sync::Arc::new(snapshot));
174 }
175 Ok(DataFrame::new(state, plan))
176}
177
178pub(crate) struct CommittedPlan {
180 state: SessionState,
181 plan: LogicalPlan,
182 views: Vec<String>,
183}
184
185pub(crate) async fn plan_committed_statement(
187 ctx: &SessionContext,
188 sql: &str,
189) -> Result<CommittedPlan, DbError> {
190 let state = ctx.state();
191 let plan = state.create_logical_plan(sql).await?;
192 if matches!(plan, LogicalPlan::Ddl(_) | LogicalPlan::Statement(_)) {
193 return Err(DbError::InvalidOperation(
194 "a committed read must be a query".into(),
195 ));
196 }
197 let scanned = scanned_tables(&plan)?;
198 if let Some(table) = scanned.live.into_iter().next() {
199 return Err(CommittedReadError::LiveTable { table }.into());
200 }
201 Ok(CommittedPlan {
202 state,
203 plan,
204 views: scanned.views,
205 })
206}
207
208impl CommittedPlan {
209 pub(crate) fn views(&self) -> &[String] {
211 &self.views
212 }
213
214 pub(crate) fn at_cut(
217 self,
218 store: &parking_lot::RwLock<MvStore>,
219 views: Arc<CommittedViews>,
220 cut: PinnedCut,
221 ) -> (DataFrame, ReadCut) {
222 let Self {
223 mut state,
224 plan,
225 views: scanned,
226 } = self;
227 let shapes = {
228 let guard = store.read();
229 scanned
230 .iter()
231 .filter_map(|view| Some((view.clone(), guard.view_shape(view)?)))
232 .collect()
233 };
234 let read_cut = ReadCut {
235 epoch: cut.epoch,
236 checkpoint_id: cut.checkpoint_id,
237 taken_at_ms: cut.taken_at_ms,
238 };
239 state
240 .config_mut()
241 .set_extension(Arc::new(CommittedStatement { views, cut, shapes }));
242 (DataFrame::new(state, plan), read_cut)
243 }
244}
245
246#[derive(Debug, Default)]
249struct ScannedTables {
250 views: Vec<String>,
251 live: Vec<String>,
252}
253
254fn scanned_tables(plan: &LogicalPlan) -> Result<ScannedTables, DataFusionError> {
255 let mut scanned = ScannedTables::default();
256 collect_scanned_tables(plan, &mut scanned)?;
257 scanned.views.sort_unstable();
258 scanned.views.dedup();
259 Ok(scanned)
260}
261
262fn collect_scanned_tables(
263 plan: &LogicalPlan,
264 scanned: &mut ScannedTables,
265) -> Result<(), DataFusionError> {
266 plan.apply_with_subqueries(|node| {
267 let LogicalPlan::TableScan(scan) = node else {
268 return Ok(TreeNodeRecursion::Continue);
269 };
270 let Ok(provider) = source_as_provider(&scan.source) else {
271 scanned.live.push(scan.table_name.to_string());
272 return Ok(TreeNodeRecursion::Continue);
273 };
274 if let Some(view) = provider.as_any().downcast_ref::<MvTableProvider>() {
275 scanned.views.push(view.name().to_owned());
276 } else if let Some(inner) = provider.get_logical_plan() {
277 collect_scanned_tables(&inner, scanned)?;
278 } else {
279 scanned.live.push(scan.table_name.to_string());
280 }
281 Ok(TreeNodeRecursion::Continue)
282 })?;
283 Ok(())
284}
285
286#[cfg(test)]
287mod tests {
288 use std::sync::Arc;
289
290 use arrow::array::{Array, Int64Array};
291 use arrow::datatypes::{DataType, Field, Schema};
292 use datafusion::datasource::MemTable;
293
294 use super::*;
295 use crate::mv_store::{MvStorageMode, ViewKey};
296
297 fn schema() -> Arc<Schema> {
298 Arc::new(Schema::new(vec![
299 Field::new("k", DataType::Int64, false),
300 Field::new("n", DataType::Int64, false),
301 ]))
302 }
303
304 fn rows(k: &[i64], n: &[i64]) -> RecordBatch {
305 RecordBatch::try_new(
306 schema(),
307 vec![
308 Arc::new(Int64Array::from(k.to_vec())),
309 Arc::new(Int64Array::from(n.to_vec())),
310 ],
311 )
312 .unwrap()
313 }
314
315 fn upserts(k: &[i64], n: &[i64]) -> RecordBatch {
317 let plain = rows(k, n);
318 let mut fields = plain.schema().fields().to_vec();
319 fields.push(Arc::new(Field::new(
320 laminar_core::changelog::WEIGHT_COLUMN,
321 DataType::Int64,
322 false,
323 )));
324 let mut columns = plain.columns().to_vec();
325 columns.push(Arc::new(Int64Array::from(vec![1; k.len()])));
326 RecordBatch::try_new(Arc::new(Schema::new(fields)), columns).unwrap()
327 }
328
329 fn two_views() -> (SessionContext, Arc<parking_lot::RwLock<MvStore>>) {
331 let store = Arc::new(parking_lot::RwLock::new(MvStore::new()));
332 let ctx = laminar_sql::create_session_context();
333 for view in ["a", "b"] {
334 store
335 .write()
336 .create_mv(
337 view,
338 schema(),
339 MvStorageMode::changelog(ViewKey::Keyed(vec![0])),
340 )
341 .unwrap();
342 store.write().update(view, &upserts(&[1, 2], &[1, 1]));
343 let provider = MvTableProvider::new(view.into(), schema(), Arc::clone(&store));
344 ctx.register_table(view, Arc::new(provider)).unwrap();
345 }
346 (ctx, store)
347 }
348
349 fn apply_second_cycle(store: &parking_lot::RwLock<MvStore>) {
351 let mut store = store.write();
352 store.update("a", &upserts(&[1, 2], &[2, 2]));
353 store.update("b", &upserts(&[1, 2], &[2, 2]));
354 }
355
356 async fn joined_counts(df: DataFrame) -> Vec<(i64, i64)> {
357 let mut out = Vec::new();
358 for batch in df.collect().await.unwrap() {
359 let a = batch
360 .column(0)
361 .as_any()
362 .downcast_ref::<Int64Array>()
363 .unwrap();
364 let b = batch
365 .column(1)
366 .as_any()
367 .downcast_ref::<Int64Array>()
368 .unwrap();
369 out.extend((0..batch.num_rows()).map(|row| (a.value(row), b.value(row))));
370 }
371 out.sort_unstable();
372 out
373 }
374
375 const JOIN: &str = "SELECT a.n, b.n FROM a JOIN b ON a.k = b.k";
376
377 #[tokio::test]
378 async fn a_statement_reads_every_view_from_the_cycle_it_was_planned_at() {
379 let (ctx, store) = two_views();
380 let df = plan_statement(&ctx, &store, JOIN).await.unwrap();
381 apply_second_cycle(&store);
382 assert_eq!(joined_counts(df).await, vec![(1, 1), (1, 1)]);
383 }
384
385 #[tokio::test]
386 async fn an_unsnapshotted_scan_reads_the_store_when_it_executes() {
387 let (ctx, store) = two_views();
388 let df = ctx.sql(JOIN).await.unwrap();
389 apply_second_cycle(&store);
390 assert_eq!(joined_counts(df).await, vec![(2, 2), (2, 2)]);
391 }
392
393 #[tokio::test]
394 async fn scanned_views_cover_subqueries_ctes_and_inlined_views() {
395 let (ctx, _store) = two_views();
396 let plain = MemTable::try_new(schema(), vec![vec![rows(&[1], &[1])]]).unwrap();
397 ctx.register_table("plain", Arc::new(plain)).unwrap();
398 ctx.sql("CREATE VIEW over_b AS SELECT k FROM b")
399 .await
400 .unwrap();
401 for (sql, expected) in [
402 ("SELECT * FROM plain", vec![]),
403 (
404 "SELECT * FROM a JOIN a AS again ON a.k = again.k",
405 vec!["a"],
406 ),
407 (
408 "SELECT k FROM plain WHERE k IN (SELECT k FROM b)",
409 vec!["b"],
410 ),
411 (
412 "WITH x AS (SELECT k FROM a) SELECT * FROM x JOIN b USING (k)",
413 vec!["a", "b"],
414 ),
415 (
416 "SELECT * FROM a WHERE k IN (SELECT k FROM over_b)",
417 vec!["a", "b"],
418 ),
419 ] {
420 let plan = ctx.state().create_logical_plan(sql).await.unwrap();
421 assert_eq!(scanned_tables(&plan).unwrap().views, expected, "{sql}");
422 }
423 }
424
425 #[tokio::test]
426 async fn ddl_keeps_the_context_execution_path() {
427 let (ctx, store) = two_views();
428 plan_statement(&ctx, &store, "CREATE VIEW over_a AS SELECT * FROM a")
429 .await
430 .unwrap();
431 let df = plan_statement(&ctx, &store, "SELECT COUNT(*) FROM over_a")
432 .await
433 .unwrap();
434 let batches = df.collect().await.unwrap();
435 let count = batches[0].column(0).as_any().downcast_ref::<Int64Array>();
436 assert_eq!(count.map(|c| c.value(0)), Some(2));
437 }
438}