1use crate::time::TimeRange;
2
3use super::{partition::Partition, view::ViewMetadata};
4use anyhow::{Context, Result};
5use async_trait::async_trait;
6use chrono::{DateTime, Utc};
7use micromegas_tracing::prelude::*;
8use sqlx::{PgPool, Row};
9use std::{fmt, sync::Arc};
10
11#[async_trait]
13pub trait QueryPartitionProvider: std::fmt::Display + Send + Sync + std::fmt::Debug {
14 async fn fetch(
16 &self,
17 view_set_name: &str,
18 view_instance_id: &str,
19 query_range: Option<TimeRange>,
20 file_schema_hash: Vec<u8>,
21 ) -> Result<Vec<Partition>>;
22}
23
24#[derive(Debug)]
26pub struct PartitionCache {
27 pub partitions: Vec<Partition>,
28 insert_range: TimeRange,
29}
30
31impl fmt::Display for PartitionCache {
32 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
33 write!(f, "{self:?}")
34 }
35}
36
37impl PartitionCache {
38 pub fn empty(insert_range: TimeRange) -> Self {
42 Self {
43 partitions: vec![],
44 insert_range,
45 }
46 }
47
48 pub fn len(&self) -> usize {
49 self.partitions.len()
50 }
51
52 pub fn is_empty(&self) -> bool {
53 self.partitions.is_empty()
54 }
55
56 #[span_fn]
60 pub async fn fetch_overlapping_insert_range(
61 pool: &sqlx::PgPool,
62 insert_range: TimeRange,
63 ) -> Result<Self> {
64 let rows = instrument_named!(
65 sqlx::query(
66 "SELECT view_set_name,
67 view_instance_id,
68 begin_insert_time,
69 end_insert_time,
70 min_event_time,
71 max_event_time,
72 updated,
73 file_path,
74 file_size,
75 file_schema_hash,
76 source_data_hash,
77 num_rows,
78 sort_order,
79 max_sort_key_time
80 FROM lakehouse_partitions
81 WHERE begin_insert_time < $1
82 AND end_insert_time > $2
83 ORDER BY begin_insert_time, file_path
84 ;",
85 )
86 .bind(insert_range.end)
87 .bind(insert_range.begin)
88 .fetch_all(pool),
89 "sql_select_overlapping_partitions"
90 )
91 .await
92 .with_context(|| "fetching partitions")?;
93 let mut partitions = vec![];
94 for r in rows {
95 let view_metadata = ViewMetadata {
96 view_set_name: Arc::new(r.try_get("view_set_name")?),
97 view_instance_id: Arc::new(r.try_get("view_instance_id")?),
98 file_schema_hash: r.try_get("file_schema_hash")?,
99 };
100 let insert_time_range = TimeRange {
102 begin: r.try_get("begin_insert_time")?,
103 end: r.try_get("end_insert_time")?,
104 };
105 let event_time_range = match (
106 r.try_get::<DateTime<Utc>, _>("min_event_time").ok(),
107 r.try_get::<DateTime<Utc>, _>("max_event_time").ok(),
108 ) {
109 (Some(begin), Some(end)) => Some(TimeRange { begin, end }),
110 (None, None) => None, (Some(_), None) | (None, Some(_)) => {
112 anyhow::bail!(
113 "Corrupt partition record: only one of min/max_event_time is NULL"
114 );
115 }
116 };
117 let partition = Partition {
118 view_metadata,
119 insert_time_range,
120 event_time_range,
121 updated: r.try_get("updated")?,
122 file_path: r.try_get::<String, _>("file_path").ok(),
123 file_size: r.try_get("file_size")?,
124 source_data_hash: r.try_get("source_data_hash")?,
125 num_rows: r.try_get("num_rows")?,
126 sort_order: r.try_get("sort_order")?,
127 max_sort_key_time: r.try_get("max_sort_key_time")?,
128 };
129 partition
130 .validate()
131 .with_context(|| "validating partition from database")?;
132 partitions.push(partition);
133 }
134 Ok(Self {
135 partitions,
136 insert_range,
137 })
138 }
139
140 #[span_fn]
142 pub async fn fetch_overlapping_insert_range_for_view(
143 pool: &sqlx::PgPool,
144 view_set_name: Arc<String>,
145 view_instance_id: Arc<String>,
146 insert_range: TimeRange,
147 ) -> Result<Self> {
148 let rows = instrument_named!(
149 sqlx::query(
150 "SELECT begin_insert_time,
151 end_insert_time,
152 min_event_time,
153 max_event_time,
154 updated,
155 file_path,
156 file_size,
157 file_schema_hash,
158 source_data_hash,
159 num_rows,
160 sort_order,
161 max_sort_key_time
162 FROM lakehouse_partitions
163 WHERE begin_insert_time < $1
164 AND end_insert_time > $2
165 AND view_set_name = $3
166 AND view_instance_id = $4
167 ORDER BY begin_insert_time, file_path
168 ;",
169 )
170 .bind(insert_range.end)
171 .bind(insert_range.begin)
172 .bind(&*view_set_name)
173 .bind(&*view_instance_id)
174 .fetch_all(pool),
175 "sql_select_overlapping_partitions_for_view"
176 )
177 .await
178 .with_context(|| "fetching partitions")?;
179 let mut partitions = vec![];
180 for r in rows {
181 let view_metadata = ViewMetadata {
182 view_set_name: view_set_name.clone(),
183 view_instance_id: view_instance_id.clone(),
184 file_schema_hash: r.try_get("file_schema_hash")?,
185 };
186 let insert_time_range = TimeRange {
188 begin: r.try_get("begin_insert_time")?,
189 end: r.try_get("end_insert_time")?,
190 };
191 let event_time_range = match (
192 r.try_get::<DateTime<Utc>, _>("min_event_time").ok(),
193 r.try_get::<DateTime<Utc>, _>("max_event_time").ok(),
194 ) {
195 (Some(begin), Some(end)) => Some(TimeRange { begin, end }),
196 (None, None) => None, (Some(_), None) | (None, Some(_)) => {
198 anyhow::bail!(
199 "Corrupt partition record: only one of min/max_event_time is NULL"
200 );
201 }
202 };
203 let partition = Partition {
204 view_metadata,
205 insert_time_range,
206 event_time_range,
207 updated: r.try_get("updated")?,
208 file_path: r.try_get::<String, _>("file_path").ok(),
209 file_size: r.try_get("file_size")?,
210 source_data_hash: r.try_get("source_data_hash")?,
211 num_rows: r.try_get("num_rows")?,
212 sort_order: r.try_get("sort_order")?,
213 max_sort_key_time: r.try_get("max_sort_key_time")?,
214 };
215 partition
216 .validate()
217 .with_context(|| "validating partition from database")?;
218 partitions.push(partition);
219 }
220 Ok(Self {
221 partitions,
222 insert_range,
223 })
224 }
225
226 pub fn filter(
228 &self,
229 view_set_name: &str,
230 view_instance_id: &str,
231 file_schema_hash: &[u8],
232 insert_range: TimeRange,
233 ) -> Self {
234 let mut partitions = vec![];
235 for part in &self.partitions {
236 if *part.view_metadata.view_set_name == view_set_name
237 && *part.view_metadata.view_instance_id == view_instance_id
238 && part.view_metadata.file_schema_hash == file_schema_hash
239 && part.begin_insert_time() < insert_range.end
240 && part.end_insert_time() > insert_range.begin
241 {
242 partitions.push(part.clone());
243 }
244 }
245 Self {
246 partitions,
247 insert_range,
248 }
249 }
250
251 pub fn filter_insert_range(&self, insert_range: TimeRange) -> Self {
253 let mut partitions = vec![];
254 for part in &self.partitions {
255 if part.begin_insert_time() < insert_range.end
256 && part.end_insert_time() > insert_range.begin
257 {
258 partitions.push(part.clone());
259 }
260 }
261 Self {
262 partitions,
263 insert_range,
264 }
265 }
266
267 pub fn filter_inside_range(
269 &self,
270 view_set_name: &str,
271 view_instance_id: &str,
272 insert_range: TimeRange,
273 ) -> Self {
274 let mut partitions = vec![];
275 for part in &self.partitions {
276 if *part.view_metadata.view_set_name == view_set_name
277 && *part.view_metadata.view_instance_id == view_instance_id
278 && part.begin_insert_time() >= insert_range.begin
279 && part.end_insert_time() <= insert_range.end
280 {
281 partitions.push(part.clone());
282 }
283 }
284 Self {
285 partitions,
286 insert_range,
287 }
288 }
289}
290
291#[async_trait]
292impl QueryPartitionProvider for PartitionCache {
293 #[span_fn]
295 async fn fetch(
296 &self,
297 view_set_name: &str,
298 view_instance_id: &str,
299 query_range: Option<TimeRange>,
300 file_schema_hash: Vec<u8>,
301 ) -> Result<Vec<Partition>> {
302 let mut partitions = vec![];
303 if let Some(range) = query_range {
304 if range.begin < self.insert_range.begin || range.end > self.insert_range.end {
305 anyhow::bail!("filtering from a result set that's not large enough");
306 }
307 for part in &self.partitions {
308 if *part.view_metadata.view_set_name == view_set_name
309 && *part.view_metadata.view_instance_id == view_instance_id
310 && part.begin_insert_time() < range.end
311 && part.end_insert_time() > range.begin
312 && part.view_metadata.file_schema_hash == file_schema_hash
313 {
314 partitions.push(part.clone());
315 }
316 }
317 } else {
318 for part in &self.partitions {
319 if *part.view_metadata.view_set_name == view_set_name
320 && *part.view_metadata.view_instance_id == view_instance_id
321 && part.view_metadata.file_schema_hash == file_schema_hash
322 {
323 partitions.push(part.clone());
324 }
325 }
326 }
327 Ok(partitions)
328 }
329}
330
331#[derive(Debug)]
333pub struct LivePartitionProvider {
334 db_pool: PgPool,
335}
336
337impl fmt::Display for LivePartitionProvider {
338 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
339 write!(f, "{self:?}")
340 }
341}
342
343impl LivePartitionProvider {
344 pub fn new(db_pool: PgPool) -> Self {
345 Self { db_pool }
346 }
347}
348
349#[async_trait]
350impl QueryPartitionProvider for LivePartitionProvider {
351 #[span_fn]
352 async fn fetch(
353 &self,
354 view_set_name: &str,
355 view_instance_id: &str,
356 query_range: Option<TimeRange>,
357 file_schema_hash: Vec<u8>,
358 ) -> Result<Vec<Partition>> {
359 let mut partitions = vec![];
360 let rows = if let Some(range) = query_range {
361 instrument_named!(
362 sqlx::query(
363 "SELECT view_set_name,
364 view_instance_id,
365 begin_insert_time,
366 end_insert_time,
367 min_event_time,
368 max_event_time,
369 updated,
370 file_path,
371 file_size,
372 file_schema_hash,
373 source_data_hash,
374 num_rows,
375 sort_order,
376 max_sort_key_time
377 FROM lakehouse_partitions
378 WHERE view_set_name = $1
379 AND view_instance_id = $2
380 -- Pruning must keep comparing max_event_time here, not max_sort_key_time: it's the
381 -- max span *end*, the only bound that conservatively covers a span whose `begin`
382 -- precedes the query range but whose `end` reaches into it. Narrowing this to
383 -- max_sort_key_time would silently prune partitions that hold matching rows.
384 AND min_event_time <= $3
385 AND max_event_time >= $4
386 AND file_schema_hash = $5
387 ORDER BY begin_insert_time, file_path
388 ;",
389 )
390 .bind(view_set_name)
391 .bind(view_instance_id)
392 .bind(range.end)
393 .bind(range.begin)
394 .bind(file_schema_hash)
395 .fetch_all(&self.db_pool),
396 "sql_select_live_partitions"
397 )
398 .await
399 .with_context(|| "listing lakehouse partitions")?
400 } else {
401 instrument_named!(
402 sqlx::query(
403 "SELECT view_set_name,
404 view_instance_id,
405 begin_insert_time,
406 end_insert_time,
407 min_event_time,
408 max_event_time,
409 updated,
410 file_path,
411 file_size,
412 file_schema_hash,
413 source_data_hash,
414 num_rows,
415 sort_order,
416 max_sort_key_time
417 FROM lakehouse_partitions
418 WHERE view_set_name = $1
419 AND view_instance_id = $2
420 AND file_schema_hash = $3
421 ORDER BY begin_insert_time, file_path
422 ;",
423 )
424 .bind(view_set_name)
425 .bind(view_instance_id)
426 .bind(file_schema_hash)
427 .fetch_all(&self.db_pool),
428 "sql_select_live_partitions"
429 )
430 .await
431 .with_context(|| "listing lakehouse partitions")?
432 };
433 for r in rows {
434 let view_metadata = ViewMetadata {
435 view_set_name: Arc::new(r.try_get("view_set_name")?),
436 view_instance_id: Arc::new(r.try_get("view_instance_id")?),
437 file_schema_hash: r.try_get("file_schema_hash")?,
438 };
439 let insert_time_range = TimeRange {
441 begin: r.try_get("begin_insert_time")?,
442 end: r.try_get("end_insert_time")?,
443 };
444 let event_time_range = match (
445 r.try_get::<DateTime<Utc>, _>("min_event_time").ok(),
446 r.try_get::<DateTime<Utc>, _>("max_event_time").ok(),
447 ) {
448 (Some(begin), Some(end)) => Some(TimeRange { begin, end }),
449 (None, None) => None, (Some(_), None) | (None, Some(_)) => {
451 anyhow::bail!(
452 "Corrupt partition record: only one of min/max_event_time is NULL"
453 );
454 }
455 };
456 let partition = Partition {
457 view_metadata,
458 insert_time_range,
459 event_time_range,
460 updated: r.try_get("updated")?,
461 file_path: r.try_get::<String, _>("file_path").ok(),
462 file_size: r.try_get("file_size")?,
463 source_data_hash: r.try_get("source_data_hash")?,
464 num_rows: r.try_get("num_rows")?,
465 sort_order: r.try_get("sort_order")?,
466 max_sort_key_time: r.try_get("max_sort_key_time")?,
467 };
468 partition
469 .validate()
470 .with_context(|| "validating partition from database")?;
471 partitions.push(partition);
472 }
473 Ok(partitions)
474 }
475}
476
477#[derive(Debug)]
479pub struct NullPartitionProvider {}
480
481impl fmt::Display for NullPartitionProvider {
482 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
483 write!(f, "{self:?}")
484 }
485}
486
487#[async_trait]
488impl QueryPartitionProvider for NullPartitionProvider {
489 async fn fetch(
490 &self,
491 _view_set_name: &str,
492 _view_instance_id: &str,
493 _query_range: Option<TimeRange>,
494 _file_schema_hash: Vec<u8>,
495 ) -> Result<Vec<Partition>> {
496 Ok(vec![])
497 }
498}