1use std::collections::HashMap;
35use std::fmt::{self, Write as _};
36use std::sync::atomic::{AtomicU64, Ordering};
37use std::sync::{Arc, Mutex};
38
39use datafusion::catalog::default_table_source::source_as_provider;
40use datafusion::common::tree_node::{TreeNode, TreeNodeRecursion};
41use datafusion::datasource::listing::ListingTable;
42use datafusion::datasource::physical_plan::FileScanConfig;
43use datafusion::datasource::source::DataSourceExec;
44use datafusion::error::Result;
45use datafusion::execution::TaskContext;
46use datafusion::logical_expr::LogicalPlan;
47use datafusion::physical_plan::ExecutionPlan;
48use datafusion::prelude::{SessionConfig, SessionContext};
49use futures::future::BoxFuture;
50use object_store::ObjectStoreExt;
51use ursa_core::{IdMap, Topology};
52
53use crate::database::{database_of_config, IndexKey, IndexStore};
54
55#[derive(Debug)]
58pub struct GraphIndex {
59 pub topology: Arc<Topology>,
60 pub ids: Arc<IdMap>,
61 weights: WeightCache,
63 stored: Option<StoredWeights>,
65}
66
67pub type WeightReader = Box<dyn Fn(&str) -> Option<Vec<Vec<f64>>> + Send + Sync>;
70
71struct StoredWeights {
73 keys: Vec<String>,
74 read: WeightReader,
75}
76
77impl fmt::Debug for StoredWeights {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 write!(f, "StoredWeights({} sets)", self.keys.len())
80 }
81}
82
83impl GraphIndex {
84 pub fn new(topology: Arc<Topology>, ids: Arc<IdMap>) -> Self {
85 Self {
86 topology,
87 ids,
88 weights: WeightCache::default(),
89 stored: None,
90 }
91 }
92
93 pub fn with_stored_weights(mut self, keys: Vec<String>, read: WeightReader) -> Self {
96 if !keys.is_empty() {
97 self.stored = Some(StoredWeights { keys, read });
98 }
99 self
100 }
101
102 pub fn weights(&self, key: &str) -> Option<Weights> {
104 self.weights.get(key)
105 }
106
107 pub fn has_stored_weights(&self, key: &str) -> bool {
109 self.stored
110 .as_ref()
111 .is_some_and(|s| s.keys.iter().any(|k| k == key))
112 }
113
114 pub fn stored_weights(&self, key: &str, n_columns: usize) -> Option<Weights> {
118 if let Some(weights) = self.weights(key) {
119 return Some(weights);
120 }
121 if !self.has_stored_weights(key) {
122 return None;
123 }
124 let columns = (self.stored.as_ref()?.read)(key)?;
125 let n_edges = self.topology.n_edges();
126 if columns.len() != n_columns || columns.iter().any(|c| c.len() != n_edges) {
127 return None;
128 }
129 let weights: Weights = Arc::new(columns.into_iter().map(Arc::new).collect());
130 self.store_weights(key.to_string(), Arc::clone(&weights));
131 Some(weights)
132 }
133
134 pub fn weight_sets(&self) -> Vec<(String, Weights)> {
136 self.weights.all()
137 }
138
139 pub fn store_weights(&self, key: String, weights: Weights) {
142 self.weights.put(key, weights)
143 }
144}
145
146pub type Weights = Arc<Vec<Arc<Vec<f64>>>>;
149
150const WEIGHT_CAPACITY: usize = 4;
154
155#[derive(Default)]
159struct WeightCache {
160 entries: Mutex<Vec<(String, Weights)>>,
161}
162
163impl fmt::Debug for WeightCache {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 let entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
166 write!(f, "WeightCache({} entries)", entries.len())
167 }
168}
169
170impl WeightCache {
171 fn get(&self, key: &str) -> Option<Weights> {
172 let mut entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
173 let i = entries.iter().position(|(k, _)| k == key)?;
174 let entry = entries.remove(i);
175 let weights = Arc::clone(&entry.1);
176 entries.push(entry);
177 Some(weights)
178 }
179
180 fn all(&self) -> Vec<(String, Weights)> {
181 let entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
182 entries
183 .iter()
184 .map(|(k, w)| (k.clone(), Arc::clone(w)))
185 .collect()
186 }
187
188 fn put(&self, key: String, weights: Weights) {
189 let mut entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
190 entries.retain(|(k, _)| *k != key);
191 if entries.len() >= WEIGHT_CAPACITY {
192 entries.remove(0);
193 }
194 entries.push((key, weights));
195 }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
200pub struct FileIdentity {
201 pub store: String,
202 pub location: String,
203 pub size: u64,
204 pub modified: i64,
206 pub e_tag: Option<String>,
207}
208
209pub fn fingerprint(plan: &LogicalPlan) -> Option<String> {
212 let mut rendered = plan.display_indent_schema().to_string();
213 let mut cacheable = true;
214 let _ = plan.apply_with_subqueries(|node| {
215 if node.expressions().iter().any(|e| e.is_volatile()) {
216 cacheable = false;
217 return Ok(TreeNodeRecursion::Stop);
218 }
219 if let LogicalPlan::TableScan(scan) = node {
220 let listing = source_as_provider(&scan.source).ok().and_then(|p| {
221 p.downcast_ref::<ListingTable>().map(|t| {
222 let paths: Vec<String> =
223 t.table_paths().iter().map(|p| p.to_string()).collect();
224 format!("{paths:?} {:?}", t.options())
225 })
226 });
227 match listing {
228 Some(listing) => {
229 let _ = write!(rendered, "\n{}: {listing}", scan.table_name);
230 }
231 None => {
232 cacheable = false;
233 return Ok(TreeNodeRecursion::Stop);
234 }
235 }
236 }
237 Ok(TreeNodeRecursion::Continue)
238 });
239 cacheable.then_some(rendered)
240}
241
242pub async fn file_identities(
245 plan: &Arc<dyn ExecutionPlan>,
246 context: &TaskContext,
247) -> Result<Vec<FileIdentity>> {
248 let mut files = Vec::new();
249 let _ = plan.apply(|node| {
250 if let Some(config) = node
251 .downcast_ref::<DataSourceExec>()
252 .and_then(|exec| exec.data_source().downcast_ref::<FileScanConfig>())
253 {
254 for group in &config.file_groups {
255 for file in group.files() {
256 files.push((
257 config.object_store_url.clone(),
258 file.object_meta.location.clone(),
259 ));
260 }
261 }
262 }
263 Ok(TreeNodeRecursion::Continue)
264 });
265 files.sort();
266 files.dedup();
267 let mut identities = Vec::with_capacity(files.len());
268 for (url, location) in files {
269 let store = context.runtime_env().object_store(&url)?;
270 let meta = store.head(&location).await?;
271 identities.push(FileIdentity {
272 store: url.to_string(),
273 location: location.to_string(),
274 size: meta.size,
275 modified: meta.last_modified.timestamp_nanos_opt().unwrap_or(i64::MAX),
276 e_tag: meta.e_tag,
277 });
278 }
279 Ok(identities)
280}
281
282const CAPACITY: usize = 16;
285
286#[derive(Debug, Default)]
288pub struct IndexCache {
289 slots: Mutex<HashMap<String, Slot>>,
290 clock: AtomicU64,
292 builds: AtomicU64,
293 loads: AtomicU64,
294}
295
296#[derive(Debug)]
297struct Slot {
298 files: Vec<FileIdentity>,
299 cell: Arc<tokio::sync::OnceCell<Arc<GraphIndex>>>,
300 used: u64,
301}
302
303impl IndexCache {
304 pub fn builds(&self) -> u64 {
306 self.builds.load(Ordering::Relaxed)
307 }
308
309 pub fn loads(&self) -> u64 {
312 self.loads.load(Ordering::Relaxed)
313 }
314
315 pub fn record_build(&self) {
317 self.builds.fetch_add(1, Ordering::Relaxed);
318 }
319
320 pub fn get(&self, key: &(String, Vec<FileIdentity>)) -> Option<Arc<GraphIndex>> {
322 let mut slots = self.slots.lock().unwrap_or_else(|e| e.into_inner());
323 let slot = slots.get_mut(&key.0).filter(|s| s.files == key.1)?;
324 let index = slot.cell.get().cloned()?;
325 slot.used = self.clock.fetch_add(1, Ordering::Relaxed);
326 Some(index)
327 }
328
329 pub async fn get_or_build(
334 &self,
335 key: Option<(String, Vec<FileIdentity>)>,
336 build: BoxFuture<'_, Result<GraphIndex>>,
337 ) -> Result<Arc<GraphIndex>> {
338 self.get_or_build_in(None, key, build).await
339 }
340
341 pub async fn get_or_build_stored(
346 &self,
347 config: &SessionConfig,
348 key: Option<(String, Vec<FileIdentity>)>,
349 build: BoxFuture<'_, Result<GraphIndex>>,
350 ) -> Result<Arc<GraphIndex>> {
351 let stored = database_of_config(config).map(|db| {
352 (
353 db.index_store(),
354 crate::options::index_storage_limit(config),
355 )
356 });
357 self.get_or_build_in(stored, key, build).await
358 }
359
360 async fn get_or_build_in(
361 &self,
362 stored: Option<(Arc<IndexStore>, u64)>,
363 key: Option<(String, Vec<FileIdentity>)>,
364 build: BoxFuture<'_, Result<GraphIndex>>,
365 ) -> Result<Arc<GraphIndex>> {
366 let Some((fingerprint, files)) = key else {
367 self.builds.fetch_add(1, Ordering::Relaxed);
368 return build.await.map(Arc::new);
369 };
370 let stored = stored.map(|(store, limit)| {
371 (
372 store,
373 limit,
374 IndexKey::new(fingerprint.clone(), files.clone()),
375 )
376 });
377 let cell = {
378 let mut slots = self.slots.lock().unwrap_or_else(|e| e.into_inner());
379 let used = self.clock.fetch_add(1, Ordering::Relaxed);
380 match slots.get_mut(&fingerprint) {
381 Some(slot) if slot.files == files => {
382 slot.used = used;
383 Arc::clone(&slot.cell)
384 }
385 _ => {
386 if !slots.contains_key(&fingerprint) && slots.len() >= CAPACITY {
387 if let Some(oldest) = slots
388 .iter()
389 .min_by_key(|(_, s)| s.used)
390 .map(|(k, _)| k.clone())
391 {
392 slots.remove(&oldest);
393 }
394 }
395 let cell = Arc::new(tokio::sync::OnceCell::new());
396 slots.insert(
397 fingerprint,
398 Slot {
399 files,
400 cell: Arc::clone(&cell),
401 used,
402 },
403 );
404 cell
405 }
406 }
407 };
408 let (builds, loads) = (&self.builds, &self.loads);
410 cell.get_or_try_init(|| async move {
411 if let Some((store, limit, key)) = &stored {
412 let (store, key, limit) = (Arc::clone(store), key.clone(), *limit);
413 let loaded = crate::run_blocking(move || store.load(&key, limit))
415 .await
416 .ok()
417 .flatten();
418 if let Some(index) = loaded {
419 loads.fetch_add(1, Ordering::Relaxed);
420 return Ok(index);
421 }
422 }
423 builds.fetch_add(1, Ordering::Relaxed);
424 let index = Arc::new(build.await?);
425 if let Some((store, limit, key)) = stored {
426 store.save(key, Arc::clone(&index), limit);
427 }
428 Ok(index)
429 })
430 .await
431 .cloned()
432 }
433}
434
435pub(crate) fn with_index_cache(config: SessionConfig) -> SessionConfig {
437 config.with_extension(Arc::new(IndexCache::default()))
438}
439
440pub fn index_builds(ctx: &SessionContext) -> u64 {
442 ctx.state()
443 .config()
444 .get_extension::<IndexCache>()
445 .map(|c| c.builds())
446 .unwrap_or(0)
447}
448
449pub fn index_loads(ctx: &SessionContext) -> u64 {
452 ctx.state()
453 .config()
454 .get_extension::<IndexCache>()
455 .map(|c| c.loads())
456 .unwrap_or(0)
457}