1use std::collections::HashMap;
38use std::fmt::{self, Write as _};
39use std::sync::atomic::{AtomicU64, Ordering};
40use std::sync::{Arc, Mutex, OnceLock};
41
42use datafusion::arrow::buffer::BooleanBuffer;
43use datafusion::catalog::default_table_source::source_as_provider;
44use datafusion::common::tree_node::{TreeNode, TreeNodeRecursion};
45use datafusion::datasource::listing::ListingTable;
46use datafusion::datasource::physical_plan::FileScanConfig;
47use datafusion::datasource::source::DataSourceExec;
48use datafusion::error::Result;
49use datafusion::execution::TaskContext;
50use datafusion::logical_expr::LogicalPlan;
51use datafusion::physical_plan::ExecutionPlan;
52use datafusion::prelude::{SessionConfig, SessionContext};
53use futures::future::BoxFuture;
54use object_store::ObjectStoreExt;
55use ursa_core::{IdMap, Topology};
56
57use crate::database::{database_of_config, IndexKey, IndexStore};
58
59#[derive(Debug)]
67pub struct GraphIndex {
68 pub topology: Arc<Topology>,
69 pub ids: Arc<IdMap>,
70 n_dropped: u64,
72 kept: Option<BooleanBuffer>,
75 rows: OnceLock<Vec<u64>>,
77 weights: WeightCache,
79 stored: Option<StoredWeights>,
81}
82
83pub type WeightReader = Box<dyn Fn(&str) -> Option<Vec<Vec<f64>>> + Send + Sync>;
86
87struct StoredWeights {
89 keys: Vec<String>,
90 read: WeightReader,
91}
92
93impl fmt::Debug for StoredWeights {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 write!(f, "StoredWeights({} sets)", self.keys.len())
96 }
97}
98
99impl GraphIndex {
100 pub fn new(topology: Arc<Topology>, ids: Arc<IdMap>) -> Self {
101 Self {
102 topology,
103 ids,
104 n_dropped: 0,
105 kept: None,
106 rows: OnceLock::new(),
107 weights: WeightCache::default(),
108 stored: None,
109 }
110 }
111
112 pub fn with_dropped(mut self, n_dropped: u64, kept: Option<BooleanBuffer>) -> Self {
115 self.n_dropped = n_dropped;
116 self.kept = kept.filter(|_| n_dropped > 0);
117 self
118 }
119
120 pub fn n_dropped(&self) -> u64 {
123 self.n_dropped
124 }
125
126 pub fn kept_rows(&self) -> Option<&BooleanBuffer> {
129 self.kept.as_ref()
130 }
131
132 pub fn edge_row(&self, edge: u32) -> u64 {
135 match &self.kept {
136 None => u64::from(edge),
137 Some(kept) => self
138 .rows
139 .get_or_init(|| kept.set_indices().map(|i| i as u64).collect())[edge as usize],
140 }
141 }
142
143 pub fn with_stored_weights(mut self, keys: Vec<String>, read: WeightReader) -> Self {
146 if !keys.is_empty() {
147 self.stored = Some(StoredWeights { keys, read });
148 }
149 self
150 }
151
152 pub fn weights(&self, key: &str) -> Option<Weights> {
154 self.weights.get(key)
155 }
156
157 pub fn has_stored_weights(&self, key: &str) -> bool {
159 self.stored
160 .as_ref()
161 .is_some_and(|s| s.keys.iter().any(|k| k == key))
162 }
163
164 pub fn stored_weights(&self, key: &str, n_columns: usize) -> Option<Weights> {
168 if let Some(weights) = self.weights(key) {
169 return Some(weights);
170 }
171 if !self.has_stored_weights(key) {
172 return None;
173 }
174 let columns = (self.stored.as_ref()?.read)(key)?;
175 let n_edges = self.topology.n_edges();
176 if columns.len() != n_columns || columns.iter().any(|c| c.len() != n_edges) {
177 return None;
178 }
179 let weights: Weights = Arc::new(columns.into_iter().map(Arc::new).collect());
180 self.store_weights(key.to_string(), Arc::clone(&weights));
181 Some(weights)
182 }
183
184 pub fn weight_sets(&self) -> Vec<(String, Weights)> {
186 self.weights.all()
187 }
188
189 pub fn store_weights(&self, key: String, weights: Weights) {
192 self.weights.put(key, weights)
193 }
194}
195
196pub type Weights = Arc<Vec<Arc<Vec<f64>>>>;
199
200const WEIGHT_CAPACITY: usize = 4;
204
205#[derive(Default)]
209struct WeightCache {
210 entries: Mutex<Vec<(String, Weights)>>,
211}
212
213impl fmt::Debug for WeightCache {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 let entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
216 write!(f, "WeightCache({} entries)", entries.len())
217 }
218}
219
220impl WeightCache {
221 fn get(&self, key: &str) -> Option<Weights> {
222 let mut entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
223 let i = entries.iter().position(|(k, _)| k == key)?;
224 let entry = entries.remove(i);
225 let weights = Arc::clone(&entry.1);
226 entries.push(entry);
227 Some(weights)
228 }
229
230 fn all(&self) -> Vec<(String, Weights)> {
231 let entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
232 entries
233 .iter()
234 .map(|(k, w)| (k.clone(), Arc::clone(w)))
235 .collect()
236 }
237
238 fn put(&self, key: String, weights: Weights) {
239 let mut entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
240 entries.retain(|(k, _)| *k != key);
241 if entries.len() >= WEIGHT_CAPACITY {
242 entries.remove(0);
243 }
244 entries.push((key, weights));
245 }
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
250pub struct FileIdentity {
251 pub store: String,
252 pub location: String,
253 pub size: u64,
254 pub modified: i64,
256 pub e_tag: Option<String>,
257}
258
259pub fn fingerprint(plan: &LogicalPlan) -> Option<String> {
262 let mut rendered = plan.display_indent_schema().to_string();
263 let mut cacheable = true;
264 let _ = plan.apply_with_subqueries(|node| {
265 if node.expressions().iter().any(|e| e.is_volatile()) {
266 cacheable = false;
267 return Ok(TreeNodeRecursion::Stop);
268 }
269 if let LogicalPlan::TableScan(scan) = node {
270 let listing = source_as_provider(&scan.source).ok().and_then(|p| {
271 p.downcast_ref::<ListingTable>().map(|t| {
272 let paths: Vec<String> =
273 t.table_paths().iter().map(|p| p.to_string()).collect();
274 format!("{paths:?} {:?}", t.options())
275 })
276 });
277 match listing {
278 Some(listing) => {
279 let _ = write!(rendered, "\n{}: {listing}", scan.table_name);
280 }
281 None => {
282 cacheable = false;
283 return Ok(TreeNodeRecursion::Stop);
284 }
285 }
286 }
287 Ok(TreeNodeRecursion::Continue)
288 });
289 cacheable.then_some(rendered)
290}
291
292pub async fn file_identities(
295 plan: &Arc<dyn ExecutionPlan>,
296 context: &TaskContext,
297) -> Result<Vec<FileIdentity>> {
298 let mut files = Vec::new();
299 let _ = plan.apply(|node| {
300 if let Some(config) = node
301 .downcast_ref::<DataSourceExec>()
302 .and_then(|exec| exec.data_source().downcast_ref::<FileScanConfig>())
303 {
304 for group in &config.file_groups {
305 for file in group.files() {
306 files.push((
307 config.object_store_url.clone(),
308 file.object_meta.location.clone(),
309 ));
310 }
311 }
312 }
313 Ok(TreeNodeRecursion::Continue)
314 });
315 files.sort();
316 files.dedup();
317 let mut identities = Vec::with_capacity(files.len());
318 for (url, location) in files {
319 let store = context.runtime_env().object_store(&url)?;
320 let meta = store.head(&location).await?;
321 identities.push(FileIdentity {
322 store: url.to_string(),
323 location: location.to_string(),
324 size: meta.size,
325 modified: meta.last_modified.timestamp_nanos_opt().unwrap_or(i64::MAX),
326 e_tag: meta.e_tag,
327 });
328 }
329 Ok(identities)
330}
331
332const CAPACITY: usize = 16;
335
336#[derive(Debug, Default)]
338pub struct IndexCache {
339 slots: Mutex<HashMap<String, Slot>>,
340 clock: AtomicU64,
342 builds: AtomicU64,
343 loads: AtomicU64,
344}
345
346#[derive(Debug)]
347struct Slot {
348 files: Vec<FileIdentity>,
349 cell: Arc<tokio::sync::OnceCell<Arc<GraphIndex>>>,
350 used: u64,
351}
352
353impl IndexCache {
354 pub fn builds(&self) -> u64 {
356 self.builds.load(Ordering::Relaxed)
357 }
358
359 pub fn loads(&self) -> u64 {
362 self.loads.load(Ordering::Relaxed)
363 }
364
365 pub fn record_build(&self) {
367 self.builds.fetch_add(1, Ordering::Relaxed);
368 }
369
370 pub fn get(&self, key: &(String, Vec<FileIdentity>)) -> Option<Arc<GraphIndex>> {
372 let mut slots = self.slots.lock().unwrap_or_else(|e| e.into_inner());
373 let slot = slots.get_mut(&key.0).filter(|s| s.files == key.1)?;
374 let index = slot.cell.get().cloned()?;
375 slot.used = self.clock.fetch_add(1, Ordering::Relaxed);
376 Some(index)
377 }
378
379 pub async fn get_or_build(
384 &self,
385 key: Option<(String, Vec<FileIdentity>)>,
386 build: BoxFuture<'_, Result<GraphIndex>>,
387 ) -> Result<Arc<GraphIndex>> {
388 self.get_or_build_in(None, key, build).await
389 }
390
391 pub async fn get_or_build_stored(
396 &self,
397 config: &SessionConfig,
398 key: Option<(String, Vec<FileIdentity>)>,
399 build: BoxFuture<'_, Result<GraphIndex>>,
400 ) -> Result<Arc<GraphIndex>> {
401 let stored = database_of_config(config).map(|db| {
402 (
403 db.index_store(),
404 crate::options::index_storage_limit(config),
405 )
406 });
407 self.get_or_build_in(stored, key, build).await
408 }
409
410 async fn get_or_build_in(
411 &self,
412 stored: Option<(Arc<IndexStore>, u64)>,
413 key: Option<(String, Vec<FileIdentity>)>,
414 build: BoxFuture<'_, Result<GraphIndex>>,
415 ) -> Result<Arc<GraphIndex>> {
416 let Some((fingerprint, files)) = key else {
417 self.builds.fetch_add(1, Ordering::Relaxed);
418 return build.await.map(Arc::new);
419 };
420 let stored = stored.map(|(store, limit)| {
421 (
422 store,
423 limit,
424 IndexKey::new(fingerprint.clone(), files.clone()),
425 )
426 });
427 let cell = {
428 let mut slots = self.slots.lock().unwrap_or_else(|e| e.into_inner());
429 let used = self.clock.fetch_add(1, Ordering::Relaxed);
430 match slots.get_mut(&fingerprint) {
431 Some(slot) if slot.files == files => {
432 slot.used = used;
433 Arc::clone(&slot.cell)
434 }
435 _ => {
436 if !slots.contains_key(&fingerprint) && slots.len() >= CAPACITY {
437 if let Some(oldest) = slots
438 .iter()
439 .min_by_key(|(_, s)| s.used)
440 .map(|(k, _)| k.clone())
441 {
442 slots.remove(&oldest);
443 }
444 }
445 let cell = Arc::new(tokio::sync::OnceCell::new());
446 slots.insert(
447 fingerprint,
448 Slot {
449 files,
450 cell: Arc::clone(&cell),
451 used,
452 },
453 );
454 cell
455 }
456 }
457 };
458 let (builds, loads) = (&self.builds, &self.loads);
460 cell.get_or_try_init(|| async move {
461 if let Some((store, limit, key)) = &stored {
462 let (store, key, limit) = (Arc::clone(store), key.clone(), *limit);
463 let loaded = crate::run_blocking(move || store.load(&key, limit))
465 .await
466 .ok()
467 .flatten();
468 if let Some(index) = loaded {
469 loads.fetch_add(1, Ordering::Relaxed);
470 return Ok(index);
471 }
472 }
473 builds.fetch_add(1, Ordering::Relaxed);
474 let index = Arc::new(build.await?);
475 if let Some((store, limit, key)) = stored {
476 store.save(key, Arc::clone(&index), limit);
477 }
478 Ok(index)
479 })
480 .await
481 .cloned()
482 }
483}
484
485pub(crate) fn with_index_cache(config: SessionConfig) -> SessionConfig {
487 config.with_extension(Arc::new(IndexCache::default()))
488}
489
490pub fn index_builds(ctx: &SessionContext) -> u64 {
492 ctx.state()
493 .config()
494 .get_extension::<IndexCache>()
495 .map(|c| c.builds())
496 .unwrap_or(0)
497}
498
499pub fn index_loads(ctx: &SessionContext) -> u64 {
502 ctx.state()
503 .config()
504 .get_extension::<IndexCache>()
505 .map(|c| c.loads())
506 .unwrap_or(0)
507}