Skip to main content

reddb_server/runtime/
result_cache_runtime.rs

1use std::collections::hash_map::Entry;
2use std::collections::{HashMap, HashSet, VecDeque};
3
4use super::{RedDBRuntime, RuntimeQueryResult, RuntimeResultCacheEntry};
5
6const RESULT_CACHE_BACKEND_KEY: &str = "runtime.result_cache.backend";
7const RESULT_CACHE_DEFAULT_BACKEND: &str = "legacy";
8const RESULT_CACHE_BLOB_NAMESPACE: &str = "runtime.result_cache";
9const RESULT_CACHE_EMBEDDED_COLLECTION: &str = "red_internal_result_cache";
10const RESULT_CACHE_TTL_SECS: u64 = 30;
11const RESULT_CACHE_MAX_ENTRIES: usize = 1000;
12const RESULT_CACHE_ENABLED_KEY: &str = "runtime.result_cache.enabled";
13const RESULT_CACHE_TTL_KEY: &str = "runtime.result_cache.ttl_seconds";
14const RESULT_CACHE_CAPACITY_KEY: &str = "runtime.result_cache.capacity_entries";
15const RESULT_CACHE_PAYLOAD_MAGIC: &[u8; 8] = b"RDRC0001";
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18enum RuntimeResultCacheBackend {
19    Legacy,
20    BlobCache,
21    Shadow,
22}
23
24fn trim_result_cache(
25    map: &mut HashMap<String, RuntimeResultCacheEntry>,
26    order: &mut VecDeque<String>,
27    max_entries: usize,
28) -> u64 {
29    let mut evicted = 0u64;
30    while map.len() > max_entries {
31        if let Some(oldest) = order.pop_front() {
32            if map.remove(&oldest).is_some() {
33                evicted += 1;
34            }
35        } else {
36            break;
37        }
38    }
39    evicted
40}
41
42fn result_cache_fingerprint(result: &RuntimeQueryResult) -> String {
43    format!(
44        "{:?}|{}|{}|{}|{}|{:?}",
45        result.result,
46        result.query,
47        result.statement,
48        result.engine,
49        result.affected_rows,
50        result.statement_type
51    )
52}
53
54fn mode_to_byte(mode: reddb_rql::modes::QueryMode) -> u8 {
55    match mode {
56        reddb_rql::modes::QueryMode::Sql => 0,
57        reddb_rql::modes::QueryMode::Gremlin => 1,
58        reddb_rql::modes::QueryMode::Cypher => 2,
59        reddb_rql::modes::QueryMode::Sparql => 3,
60        reddb_rql::modes::QueryMode::Path => 4,
61        reddb_rql::modes::QueryMode::Natural => 5,
62        reddb_rql::modes::QueryMode::Unknown => 255,
63    }
64}
65
66fn mode_from_byte(byte: u8) -> Option<reddb_rql::modes::QueryMode> {
67    match byte {
68        0 => Some(reddb_rql::modes::QueryMode::Sql),
69        1 => Some(reddb_rql::modes::QueryMode::Gremlin),
70        2 => Some(reddb_rql::modes::QueryMode::Cypher),
71        3 => Some(reddb_rql::modes::QueryMode::Sparql),
72        4 => Some(reddb_rql::modes::QueryMode::Path),
73        5 => Some(reddb_rql::modes::QueryMode::Natural),
74        255 => Some(reddb_rql::modes::QueryMode::Unknown),
75        _ => None,
76    }
77}
78
79fn result_cache_static_str(value: &str) -> Option<&'static str> {
80    match value {
81        "select" => Some("select"),
82        "materialized-graph" => Some("materialized-graph"),
83        "runtime-red-schema" => Some("runtime-red-schema"),
84        "runtime-fdw" => Some("runtime-fdw"),
85        "runtime-table-rls" => Some("runtime-table-rls"),
86        "runtime-table" => Some("runtime-table"),
87        "runtime-join-rls" => Some("runtime-join-rls"),
88        "runtime-join" => Some("runtime-join"),
89        "runtime-vector" => Some("runtime-vector"),
90        "runtime-hybrid" => Some("runtime-hybrid"),
91        "runtime-secret" => Some("runtime-secret"),
92        "runtime-config" => Some("runtime-config"),
93        "runtime-tenant" => Some("runtime-tenant"),
94        "runtime-explain" => Some("runtime-explain"),
95        "runtime-tree" => Some("runtime-tree"),
96        "runtime-kv" => Some("runtime-kv"),
97        "runtime-queue" => Some("runtime-queue"),
98        _ => None,
99    }
100}
101
102fn write_u32(out: &mut Vec<u8>, value: usize) -> Option<()> {
103    let value = u32::try_from(value).ok()?;
104    out.extend_from_slice(&value.to_le_bytes());
105    Some(())
106}
107
108fn write_string(out: &mut Vec<u8>, value: &str) -> Option<()> {
109    write_u32(out, value.len())?;
110    out.extend_from_slice(value.as_bytes());
111    Some(())
112}
113
114fn write_bytes(out: &mut Vec<u8>, value: &[u8]) -> Option<()> {
115    write_u32(out, value.len())?;
116    out.extend_from_slice(value);
117    Some(())
118}
119
120fn read_u8(input: &mut &[u8]) -> Option<u8> {
121    let (&value, rest) = input.split_first()?;
122    *input = rest;
123    Some(value)
124}
125
126fn read_u32(input: &mut &[u8]) -> Option<usize> {
127    if input.len() < 4 {
128        return None;
129    }
130    let value = u32::from_le_bytes(input[..4].try_into().ok()?) as usize;
131    *input = &input[4..];
132    Some(value)
133}
134
135fn read_u64(input: &mut &[u8]) -> Option<u64> {
136    if input.len() < 8 {
137        return None;
138    }
139    let value = u64::from_le_bytes(input[..8].try_into().ok()?);
140    *input = &input[8..];
141    Some(value)
142}
143
144fn read_string(input: &mut &[u8]) -> Option<String> {
145    let len = read_u32(input)?;
146    if input.len() < len {
147        return None;
148    }
149    let value = String::from_utf8(input[..len].to_vec()).ok()?;
150    *input = &input[len..];
151    Some(value)
152}
153
154fn read_bytes<'a>(input: &mut &'a [u8]) -> Option<&'a [u8]> {
155    let len = read_u32(input)?;
156    if input.len() < len {
157        return None;
158    }
159    let value = &input[..len];
160    *input = &input[len..];
161    Some(value)
162}
163
164fn encode_result_cache_payload(entry: &RuntimeResultCacheEntry) -> Option<Vec<u8>> {
165    let result = &entry.result;
166    // Execution measurements must not survive as a persisted cached execution.
167    if result.result.stats.vector.is_some()
168        || result.result.pre_serialized_json.is_some()
169        || result_cache_static_str(result.statement).is_none()
170        || result_cache_static_str(result.engine).is_none()
171        || result_cache_static_str(result.statement_type).is_none()
172        || result.result.records.iter().any(|record| {
173            !record.nodes.is_empty()
174                || !record.edges.is_empty()
175                || !record.paths.is_empty()
176                || !record.vector_results.is_empty()
177        })
178    {
179        return None;
180    }
181
182    let mut out = Vec::new();
183    out.extend_from_slice(RESULT_CACHE_PAYLOAD_MAGIC);
184    write_string(&mut out, &result.query)?;
185    out.push(mode_to_byte(result.mode));
186    write_string(&mut out, result.statement)?;
187    write_string(&mut out, result.engine)?;
188    out.extend_from_slice(&result.affected_rows.to_le_bytes());
189    write_string(&mut out, result.statement_type)?;
190
191    write_u32(&mut out, result.result.columns.len())?;
192    for column in &result.result.columns {
193        write_string(&mut out, column)?;
194    }
195    out.extend_from_slice(&result.result.stats.nodes_scanned.to_le_bytes());
196    out.extend_from_slice(&result.result.stats.edges_scanned.to_le_bytes());
197    out.extend_from_slice(&result.result.stats.rows_scanned.to_le_bytes());
198    out.extend_from_slice(&result.result.stats.segments_total.to_le_bytes());
199    out.extend_from_slice(&result.result.stats.segments_scanned.to_le_bytes());
200    out.extend_from_slice(&result.result.stats.segments_pruned.to_le_bytes());
201    out.extend_from_slice(&result.result.stats.exec_time_us.to_le_bytes());
202
203    write_u32(&mut out, result.result.records.len())?;
204    for record in &result.result.records {
205        let fields = record.iter_fields().collect::<Vec<_>>();
206        write_u32(&mut out, fields.len())?;
207        for (name, value) in fields {
208            write_string(&mut out, name)?;
209            let mut encoded = Vec::new();
210            reddb_types::value_codec::encode(value, &mut encoded);
211            write_bytes(&mut out, &encoded)?;
212        }
213    }
214
215    write_u32(&mut out, entry.scopes.len())?;
216    for scope in &entry.scopes {
217        write_string(&mut out, scope)?;
218    }
219    Some(out)
220}
221
222fn decode_result_cache_payload(mut input: &[u8]) -> Option<(RuntimeQueryResult, HashSet<String>)> {
223    if input.len() < RESULT_CACHE_PAYLOAD_MAGIC.len()
224        || &input[..RESULT_CACHE_PAYLOAD_MAGIC.len()] != RESULT_CACHE_PAYLOAD_MAGIC
225    {
226        return None;
227    }
228    input = &input[RESULT_CACHE_PAYLOAD_MAGIC.len()..];
229
230    let query = read_string(&mut input)?;
231    let mode = mode_from_byte(read_u8(&mut input)?)?;
232    let statement = result_cache_static_str(&read_string(&mut input)?)?;
233    let engine = result_cache_static_str(&read_string(&mut input)?)?;
234    let affected_rows = read_u64(&mut input)?;
235    let statement_type = result_cache_static_str(&read_string(&mut input)?)?;
236
237    let mut columns = Vec::new();
238    for _ in 0..read_u32(&mut input)? {
239        columns.push(read_string(&mut input)?);
240    }
241    let stats = crate::storage::query::unified::QueryStats {
242        nodes_scanned: read_u64(&mut input)?,
243        edges_scanned: read_u64(&mut input)?,
244        rows_scanned: read_u64(&mut input)?,
245        segments_total: read_u64(&mut input)?,
246        segments_scanned: read_u64(&mut input)?,
247        segments_pruned: read_u64(&mut input)?,
248        exec_time_us: read_u64(&mut input)?,
249        vector: None,
250    };
251
252    let mut records = Vec::new();
253    for _ in 0..read_u32(&mut input)? {
254        let mut record = crate::storage::query::unified::UnifiedRecord::new();
255        for _ in 0..read_u32(&mut input)? {
256            let name = read_string(&mut input)?;
257            let bytes = read_bytes(&mut input)?;
258            let (value, used) = reddb_types::value_codec::decode(bytes).ok()?;
259            if used != bytes.len() {
260                return None;
261            }
262            record.set_owned(name, value);
263        }
264        records.push(record);
265    }
266
267    let mut scopes = HashSet::new();
268    for _ in 0..read_u32(&mut input)? {
269        scopes.insert(read_string(&mut input)?);
270    }
271    if !input.is_empty() {
272        return None;
273    }
274
275    Some((
276        RuntimeQueryResult {
277            query,
278            mode,
279            statement,
280            engine,
281            result: crate::storage::query::unified::UnifiedResult {
282                columns,
283                records,
284                stats,
285                pre_serialized_json: None,
286            },
287            affected_rows,
288            statement_type,
289            bookmark: None,
290            notice: None,
291        },
292        scopes,
293    ))
294}
295
296impl RedDBRuntime {
297    fn result_cache_backend(&self) -> RuntimeResultCacheBackend {
298        match self
299            .config_string(RESULT_CACHE_BACKEND_KEY, RESULT_CACHE_DEFAULT_BACKEND)
300            .as_str()
301        {
302            "blob_cache" => RuntimeResultCacheBackend::BlobCache,
303            "shadow" => RuntimeResultCacheBackend::Shadow,
304            _ => RuntimeResultCacheBackend::Legacy,
305        }
306    }
307
308    fn result_cache_enabled(&self) -> bool {
309        self.config_bool(RESULT_CACHE_ENABLED_KEY, true)
310    }
311
312    fn result_cache_ttl_secs(&self) -> u64 {
313        self.config_u64(RESULT_CACHE_TTL_KEY, RESULT_CACHE_TTL_SECS)
314    }
315
316    fn result_cache_capacity(&self) -> usize {
317        self.config_u64(RESULT_CACHE_CAPACITY_KEY, RESULT_CACHE_MAX_ENTRIES as u64) as usize
318    }
319
320    pub fn result_cache_metrics(&self) -> (u64, u64, u64) {
321        use std::sync::atomic::Ordering::Relaxed;
322        (
323            self.inner.result_cache_hits.load(Relaxed),
324            self.inner.result_cache_misses.load(Relaxed),
325            self.inner.result_cache_evictions.load(Relaxed),
326        )
327    }
328
329    fn record_result_cache_evictions(&self, evicted: u64) {
330        if evicted > 0 {
331            self.inner
332                .result_cache_evictions
333                .fetch_add(evicted, std::sync::atomic::Ordering::Relaxed);
334        }
335    }
336
337    pub(super) fn get_result_cache_entry(&self, key: &str) -> Option<RuntimeQueryResult> {
338        if !self.result_cache_enabled() {
339            return None;
340        }
341        let hit = self.get_result_cache_entry_inner(key);
342        let counter = if hit.is_some() {
343            &self.inner.result_cache_hits
344        } else {
345            &self.inner.result_cache_misses
346        };
347        counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
348        hit
349    }
350
351    fn get_result_cache_entry_inner(&self, key: &str) -> Option<RuntimeQueryResult> {
352        match self.result_cache_backend() {
353            RuntimeResultCacheBackend::Legacy => self.get_legacy_result_cache_entry(key),
354            RuntimeResultCacheBackend::BlobCache => self.get_blob_result_cache_entry(key),
355            RuntimeResultCacheBackend::Shadow => {
356                let legacy = self.get_legacy_result_cache_entry(key);
357                let blob = self.get_blob_result_cache_entry(key);
358                if let (Some(ref legacy), Some(ref blob)) = (&legacy, &blob) {
359                    if result_cache_fingerprint(legacy) != result_cache_fingerprint(blob) {
360                        self.inner
361                            .result_cache_shadow_divergences
362                            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
363                        tracing::warn!(
364                            key,
365                            metric = crate::runtime::METRIC_CACHE_SHADOW_DIVERGENCE_TOTAL,
366                            "result cache shadow backend diverged from legacy"
367                        );
368                    }
369                }
370                legacy
371            }
372        }
373    }
374
375    fn get_legacy_result_cache_entry(&self, key: &str) -> Option<RuntimeQueryResult> {
376        let ttl = self.result_cache_ttl_secs();
377        let cache = self.inner.result_cache.read();
378        cache.0.get(key).and_then(|entry| {
379            if entry.cached_at.elapsed().as_secs() < ttl {
380                Some(entry.result.clone())
381            } else {
382                None
383            }
384        })
385    }
386
387    fn get_blob_result_cache_entry(&self, key: &str) -> Option<RuntimeQueryResult> {
388        if let Some(result) = self.get_blob_result_cache_entry_from_blob_cache(key) {
389            return Some(result);
390        }
391        // One-way compatibility read for databases written before the generic
392        // single-file L2 existed. A hit is promoted into the new backend; new
393        // writes never duplicate payloads in this legacy collection.
394        if self.inner.embedded_single_file {
395            if let Some(bytes) = self.get_embedded_result_cache_payload(key) {
396                let policy = crate::storage::cache::BlobCachePolicy::default()
397                    .ttl_ms(self.result_cache_ttl_secs() * 1000)
398                    .priority(200);
399                let put = crate::storage::cache::BlobCachePut::new(bytes).with_policy(policy);
400                if self
401                    .inner
402                    .result_blob_cache
403                    .put(RESULT_CACHE_BLOB_NAMESPACE, key, put)
404                    .is_err()
405                {
406                    return None;
407                }
408                return self.get_blob_result_cache_entry_from_blob_cache(key);
409            }
410        }
411        None
412    }
413
414    fn get_blob_result_cache_entry_from_blob_cache(&self, key: &str) -> Option<RuntimeQueryResult> {
415        let hit = self
416            .inner
417            .result_blob_cache
418            .get(RESULT_CACHE_BLOB_NAMESPACE, key)?;
419        {
420            let cache = self.inner.result_blob_entries.read();
421            if let Some(entry) = cache.0.get(key) {
422                return Some(entry.result.clone());
423            }
424        }
425
426        let (result, scopes) = decode_result_cache_payload(hit.value())?;
427        let mut cache = self.inner.result_blob_entries.write();
428        let (ref mut map, ref mut order) = *cache;
429        let new_entry = RuntimeResultCacheEntry {
430            result: result.clone(),
431            cached_at: std::time::Instant::now(),
432            scopes,
433        };
434        // Single hash lookup: drive the LRU-order push_back only on Vacant.
435        match map.entry(key.to_string()) {
436            Entry::Occupied(mut slot) => {
437                slot.insert(new_entry);
438            }
439            Entry::Vacant(slot) => {
440                order.push_back(slot.key().clone());
441                slot.insert(new_entry);
442            }
443        }
444        let evicted = trim_result_cache(map, order, self.result_cache_capacity());
445        drop(cache);
446        self.record_result_cache_evictions(evicted);
447        Some(result)
448    }
449
450    fn get_embedded_result_cache_payload(&self, key: &str) -> Option<Vec<u8>> {
451        let manager = self
452            .inner
453            .db
454            .store()
455            .get_collection(RESULT_CACHE_EMBEDDED_COLLECTION)?;
456        let mut latest: Option<(u64, Vec<u8>)> = None;
457        manager.for_each_entity(|entity| {
458            let Some(row) = entity.data.as_row() else {
459                return true;
460            };
461            let namespace_matches = row.get_field("namespace").and_then(|value| match value {
462                reddb_types::Value::Text(value) => Some(value.as_ref()),
463                _ => None,
464            }) == Some(RESULT_CACHE_BLOB_NAMESPACE);
465            let key_matches = row.get_field("key").and_then(|value| match value {
466                reddb_types::Value::Text(value) => Some(value.as_ref()),
467                _ => None,
468            }) == Some(key);
469            if namespace_matches && key_matches {
470                if let Some(reddb_types::Value::Blob(payload)) = row.get_field("payload") {
471                    let id = entity.id.raw();
472                    if latest
473                        .as_ref()
474                        .is_none_or(|(latest_id, _)| id >= *latest_id)
475                    {
476                        latest = Some((id, payload.clone()));
477                    }
478                }
479            }
480            true
481        });
482        latest.map(|(_, payload)| payload)
483    }
484
485    pub(super) fn put_result_cache_entry(&self, key: &str, entry: RuntimeResultCacheEntry) {
486        if !self.result_cache_enabled() {
487            return;
488        }
489        match self.result_cache_backend() {
490            RuntimeResultCacheBackend::Legacy => self.put_legacy_result_cache_entry(key, entry),
491            RuntimeResultCacheBackend::BlobCache => self.put_blob_result_cache_entry(key, entry),
492            RuntimeResultCacheBackend::Shadow => {
493                self.put_legacy_result_cache_entry(key, entry.clone());
494                self.put_blob_result_cache_entry(key, entry);
495            }
496        }
497    }
498
499    fn put_legacy_result_cache_entry(&self, key: &str, entry: RuntimeResultCacheEntry) {
500        let capacity = self.result_cache_capacity();
501        let mut cache = self.inner.result_cache.write();
502        let (ref mut map, ref mut order) = *cache;
503        // Single hash lookup: drive the LRU-order push_back only on Vacant.
504        match map.entry(key.to_string()) {
505            Entry::Occupied(mut slot) => {
506                slot.insert(entry);
507            }
508            Entry::Vacant(slot) => {
509                order.push_back(slot.key().clone());
510                slot.insert(entry);
511            }
512        }
513        let evicted = trim_result_cache(map, order, capacity);
514        drop(cache);
515        self.record_result_cache_evictions(evicted);
516    }
517
518    fn put_blob_result_cache_entry(&self, key: &str, entry: RuntimeResultCacheEntry) {
519        let policy = crate::storage::cache::BlobCachePolicy::default()
520            .ttl_ms(self.result_cache_ttl_secs() * 1000)
521            .priority(200);
522        let dependencies = entry.scopes.iter().cloned().collect::<Vec<_>>();
523        let bytes = encode_result_cache_payload(&entry)
524            .unwrap_or_else(|| result_cache_fingerprint(&entry.result).into_bytes());
525        let put = crate::storage::cache::BlobCachePut::new(bytes)
526            .with_dependencies(dependencies)
527            .with_policy(policy);
528        if self
529            .inner
530            .result_blob_cache
531            .put(RESULT_CACHE_BLOB_NAMESPACE, key, put)
532            .is_err()
533        {
534            return;
535        }
536
537        let capacity = self.result_cache_capacity();
538        let mut cache = self.inner.result_blob_entries.write();
539        let (ref mut map, ref mut order) = *cache;
540        // Single hash lookup: drive the LRU-order push_back only on Vacant.
541        match map.entry(key.to_string()) {
542            Entry::Occupied(mut slot) => {
543                slot.insert(entry);
544            }
545            Entry::Vacant(slot) => {
546                order.push_back(slot.key().clone());
547                slot.insert(entry);
548            }
549        }
550        let evicted = trim_result_cache(map, order, capacity);
551        drop(cache);
552        self.record_result_cache_evictions(evicted);
553    }
554
555    fn invalidate_embedded_result_cache(&self) {
556        let Some(manager) = self
557            .inner
558            .db
559            .store()
560            .get_collection(RESULT_CACHE_EMBEDDED_COLLECTION)
561        else {
562            return;
563        };
564        let ids = manager
565            .query_all(|_| true)
566            .into_iter()
567            .map(|entity| entity.id)
568            .collect::<Vec<_>>();
569        if !ids.is_empty() {
570            let _ = self
571                .inner
572                .db
573                .store()
574                .delete_batch(RESULT_CACHE_EMBEDDED_COLLECTION, &ids);
575        }
576    }
577
578    fn invalidate_embedded_result_cache_for_scope(&self, scope: &str) {
579        let Some(manager) = self
580            .inner
581            .db
582            .store()
583            .get_collection(RESULT_CACHE_EMBEDDED_COLLECTION)
584        else {
585            return;
586        };
587        let ids = manager
588            .query_all(|entity| {
589                entity
590                    .data
591                    .as_row()
592                    .and_then(|row| row.get_field("scopes"))
593                    .and_then(|value| match value {
594                        reddb_types::Value::Text(value) => Some(value.as_ref()),
595                        _ => None,
596                    })
597                    .is_some_and(|scopes| scopes.lines().any(|entry| entry == scope))
598            })
599            .into_iter()
600            .map(|entity| entity.id)
601            .collect::<Vec<_>>();
602        if !ids.is_empty() {
603            let _ = self
604                .inner
605                .db
606                .store()
607                .delete_batch(RESULT_CACHE_EMBEDDED_COLLECTION, &ids);
608        }
609    }
610
611    pub fn result_cache_shadow_divergences(&self) -> u64 {
612        self.inner
613            .result_cache_shadow_divergences
614            .load(std::sync::atomic::Ordering::Relaxed)
615    }
616
617    pub fn invalidate_result_cache(&self) {
618        self.invalidate_result_cache_process_only();
619        if self.inner.embedded_single_file {
620            self.invalidate_embedded_result_cache();
621        }
622    }
623
624    pub(crate) fn invalidate_result_cache_process_only(&self) {
625        let mut cache = self.inner.result_cache.write();
626        cache.0.clear();
627        cache.1.clear();
628        let mut blob_entries = self.inner.result_blob_entries.write();
629        blob_entries.0.clear();
630        blob_entries.1.clear();
631        self.inner
632            .result_blob_cache
633            .invalidate_namespace(RESULT_CACHE_BLOB_NAMESPACE);
634        let mut ask_entries = self.inner.ask_answer_cache_entries.write();
635        ask_entries.0.clear();
636        ask_entries.1.clear();
637        self.inner
638            .result_blob_cache
639            .invalidate_namespace(super::ASK_ANSWER_CACHE_NAMESPACE);
640    }
641
642    pub(crate) fn invalidate_result_cache_for_table(&self, table: &str) {
643        let legacy_has_match = {
644            let cache = self.inner.result_cache.read();
645            let (ref map, _) = *cache;
646            !map.is_empty() && map.values().any(|entry| entry.scopes.contains(table))
647        };
648        let blob_has_match = {
649            let cache = self.inner.result_blob_entries.read();
650            let (ref map, _) = *cache;
651            !map.is_empty() && map.values().any(|entry| entry.scopes.contains(table))
652        };
653        if legacy_has_match {
654            let mut cache = self.inner.result_cache.write();
655            let (ref mut map, ref mut order) = *cache;
656            map.retain(|_, entry| !entry.scopes.contains(table));
657            order.retain(|key| map.contains_key(key));
658        }
659
660        if matches!(
661            self.result_cache_backend(),
662            RuntimeResultCacheBackend::BlobCache | RuntimeResultCacheBackend::Shadow
663        ) {
664            let mut blob_entries = self.inner.result_blob_entries.write();
665            let (ref mut blob_map, ref mut blob_order) = *blob_entries;
666            blob_map.clear();
667            blob_order.clear();
668            self.inner
669                .result_blob_cache
670                .invalidate_namespace(RESULT_CACHE_BLOB_NAMESPACE);
671            if self.inner.embedded_single_file {
672                self.invalidate_embedded_result_cache();
673            }
674        } else if blob_has_match {
675            let mut blob_entries = self.inner.result_blob_entries.write();
676            let (ref mut blob_map, ref mut blob_order) = *blob_entries;
677            blob_map.retain(|_, entry| !entry.scopes.contains(table));
678            blob_order.retain(|key| blob_map.contains_key(key));
679            if self.inner.embedded_single_file {
680                self.invalidate_embedded_result_cache_for_scope(table);
681            }
682        }
683        let mut ask_entries = self.inner.ask_answer_cache_entries.write();
684        ask_entries.0.clear();
685        ask_entries.1.clear();
686        self.inner
687            .result_blob_cache
688            .invalidate_namespace(super::ASK_ANSWER_CACHE_NAMESPACE);
689    }
690}