Skip to main content

reddb_server/runtime/
impl_probabilistic.rs

1//! Execution of probabilistic data structure commands (HLL, SKETCH, FILTER)
2
3use super::*;
4use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
5
6const PROB_HLL_STATE_PREFIX: &str = "red.probabilistic.hll.";
7const PROB_SKETCH_STATE_PREFIX: &str = "red.probabilistic.sketch.";
8const PROB_FILTER_STATE_PREFIX: &str = "red.probabilistic.filter.";
9const PROB_ENCODING_MARKER_KEY: &str = "red.probabilistic.state_encoding";
10const PROB_ENCODING_RAW_V1: &str = "raw-v1";
11const PROB_WAL_KIND_HLL: u8 = 1;
12const PROB_WAL_KIND_SKETCH: u8 = 2;
13const PROB_WAL_KIND_FILTER: u8 = 3;
14const PROB_WAL_OP_ADD: u8 = 1;
15const PROB_WAL_OP_DELETE: u8 = 2;
16const PROB_WAL_OP_SNAPSHOT: u8 = 3;
17const PROB_WAL_OP_DROP: u8 = 4;
18
19fn probabilistic_read<'a, T>(lock: &'a RwLock<T>, _name: &str) -> RwLockReadGuard<'a, T> {
20    lock.read()
21}
22
23fn probabilistic_write<'a, T>(lock: &'a RwLock<T>, _name: &str) -> RwLockWriteGuard<'a, T> {
24    lock.write()
25}
26
27fn probabilistic_collection_contract(
28    name: &str,
29    model: crate::catalog::CollectionModel,
30) -> crate::physical::CollectionContract {
31    let now = crate::utils::now_unix_millis() as u128;
32    crate::physical::CollectionContract {
33        name: name.to_string(),
34        declared_model: model,
35        schema_mode: crate::catalog::SchemaMode::Dynamic,
36        origin: crate::physical::ContractOrigin::Explicit,
37        version: 1,
38        created_at_unix_ms: now,
39        updated_at_unix_ms: now,
40        default_ttl_ms: None,
41        vector_dimension: None,
42        vector_metric: None,
43        context_index_fields: Vec::new(),
44        declared_columns: Vec::new(),
45        table_def: None,
46        timestamps_enabled: false,
47        context_index_enabled: false,
48        metrics_raw_retention_ms: None,
49        metrics_rollup_policies: Vec::new(),
50        metrics_tenant_identity: None,
51        metrics_namespace: None,
52        append_only: false,
53        subscriptions: Vec::new(),
54        analytics_config: Vec::new(),
55        session_key: None,
56        session_gap_ms: None,
57        retention_duration_ms: None,
58        analytical_storage: None,
59
60        ai_policy: None,
61    }
62}
63
64fn hll_precision_mismatch_error(
65    operation: &str,
66    expected_name: &str,
67    expected_precision: u8,
68    actual_name: &str,
69    actual_precision: u8,
70) -> RedDBError {
71    RedDBError::Query(format!(
72        "HLL {operation} requires matching precision; '{expected_name}' uses precision {expected_precision}, but '{actual_name}' uses precision {actual_precision}"
73    ))
74}
75
76enum ProbabilisticReadProjection {
77    Cardinality { label: String },
78    Freq { element: String, label: String },
79    Contains { element: String, label: String },
80}
81
82struct ProbabilisticStateEntry {
83    name: String,
84    bytes: Vec<u8>,
85    migrated_from_legacy_hex: bool,
86}
87
88impl RedDBRuntime {
89    pub(crate) fn load_probabilistic_state(&self) -> RedDBResult<()> {
90        let reject_legacy_hex = self.probabilistic_raw_encoding_marker();
91        let mut migrated_legacy_hex = false;
92        {
93            let entries = self.latest_probabilistic_state_entries(
94                PROB_HLL_STATE_PREFIX,
95                "HLL",
96                reject_legacy_hex,
97            )?;
98            let mut hlls =
99                probabilistic_write(&self.inner.probabilistic.hlls, "probabilistic HLL store");
100            for entry in entries {
101                let Some(hll) = crate::storage::primitives::hyperloglog::HyperLogLog::from_bytes(
102                    entry.bytes.clone(),
103                ) else {
104                    return Err(RedDBError::Internal(format!(
105                        "invalid persisted HLL state for '{}'",
106                        entry.name
107                    )));
108                };
109                if entry.migrated_from_legacy_hex {
110                    self.persist_probabilistic_blob(
111                        PROB_HLL_STATE_PREFIX,
112                        &entry.name,
113                        &entry.bytes,
114                    )?;
115                    migrated_legacy_hex = true;
116                }
117                hlls.insert(entry.name, hll);
118            }
119        }
120
121        {
122            let entries = self.latest_probabilistic_state_entries(
123                PROB_SKETCH_STATE_PREFIX,
124                "SKETCH",
125                reject_legacy_hex,
126            )?;
127            let mut sketches = probabilistic_write(
128                &self.inner.probabilistic.sketches,
129                "probabilistic sketch store",
130            );
131            for entry in entries {
132                let sketch =
133                    crate::storage::primitives::count_min_sketch::CountMinSketch::from_bytes(
134                        &entry.bytes,
135                    )
136                    .ok_or_else(|| {
137                        RedDBError::Internal(format!(
138                            "invalid persisted SKETCH state for '{}'",
139                            entry.name
140                        ))
141                    })?;
142                if entry.migrated_from_legacy_hex {
143                    self.persist_probabilistic_blob(
144                        PROB_SKETCH_STATE_PREFIX,
145                        &entry.name,
146                        &entry.bytes,
147                    )?;
148                    migrated_legacy_hex = true;
149                }
150                sketches.insert(entry.name, sketch);
151            }
152        }
153
154        {
155            let entries = self.latest_probabilistic_state_entries(
156                PROB_FILTER_STATE_PREFIX,
157                "FILTER",
158                reject_legacy_hex,
159            )?;
160            let mut filters = probabilistic_write(
161                &self.inner.probabilistic.filters,
162                "probabilistic filter store",
163            );
164            for entry in entries {
165                let filter = crate::storage::primitives::cuckoo_filter::CuckooFilter::from_bytes(
166                    &entry.bytes,
167                )
168                .ok_or_else(|| {
169                    RedDBError::Internal(format!(
170                        "invalid persisted FILTER state for '{}'",
171                        entry.name
172                    ))
173                })?;
174                if entry.migrated_from_legacy_hex {
175                    self.persist_probabilistic_blob(
176                        PROB_FILTER_STATE_PREFIX,
177                        &entry.name,
178                        &entry.bytes,
179                    )?;
180                    migrated_legacy_hex = true;
181                }
182                filters.insert(entry.name, filter);
183            }
184        }
185
186        if migrated_legacy_hex {
187            self.persist_probabilistic_encoding_marker();
188        }
189        self.apply_replayed_probabilistic_deltas()?;
190        Ok(())
191    }
192
193    fn latest_probabilistic_state_entries(
194        &self,
195        prefix: &str,
196        kind: &str,
197        reject_legacy_hex: bool,
198    ) -> RedDBResult<Vec<ProbabilisticStateEntry>> {
199        let Some(manager) = self.inner.db.store().get_collection("red_config") else {
200            return Ok(Vec::new());
201        };
202        let mut latest: std::collections::HashMap<String, (u64, Option<Value>)> =
203            std::collections::HashMap::new();
204        for entity in manager.query_all(|_| true) {
205            let EntityData::Row(row) = &entity.data else {
206                continue;
207            };
208            let Some(named) = &row.named else {
209                continue;
210            };
211            let Some(Value::Text(key)) = named.get("key") else {
212                continue;
213            };
214            let Some(encoded_name) = key.strip_prefix(prefix) else {
215                continue;
216            };
217            let value = match named.get("value") {
218                Some(Value::Blob(value)) => Some(Value::Blob(value.clone())),
219                Some(Value::Text(value)) => Some(Value::Text(value.clone())),
220                Some(Value::Null) => None,
221                _ => continue,
222            };
223            let entity_id = entity.id.raw();
224            match latest.get(encoded_name) {
225                Some((existing_id, _)) if *existing_id > entity_id => {}
226                _ => {
227                    latest.insert(encoded_name.to_string(), (entity_id, value));
228                }
229            }
230        }
231
232        let mut entries = Vec::new();
233        for (encoded_name, (_, value)) in latest {
234            let Some(value) = value else {
235                continue;
236            };
237            let Some(name) = hex::decode(&encoded_name)
238                .ok()
239                .and_then(|bytes| String::from_utf8(bytes).ok())
240            else {
241                continue;
242            };
243            match value {
244                Value::Blob(bytes) => entries.push(ProbabilisticStateEntry {
245                    name,
246                    bytes,
247                    migrated_from_legacy_hex: false,
248                }),
249                Value::Text(data_hex) if reject_legacy_hex => {
250                    return Err(RedDBError::Internal(format!(
251                        "legacy hex-encoded {kind} state for '{name}' is rejected after probabilistic state migrated to raw bytes"
252                    )));
253                }
254                Value::Text(data_hex) => {
255                    let bytes = hex::decode(data_hex.as_ref()).map_err(|err| {
256                        RedDBError::Internal(format!(
257                            "invalid legacy hex-encoded {kind} state for '{name}': {err}"
258                        ))
259                    })?;
260                    entries.push(ProbabilisticStateEntry {
261                        name,
262                        bytes,
263                        migrated_from_legacy_hex: true,
264                    });
265                }
266                _ => {}
267            }
268        }
269        Ok(entries)
270    }
271
272    fn probabilistic_raw_encoding_marker(&self) -> bool {
273        let Some(manager) = self.inner.db.store().get_collection("red_config") else {
274            return false;
275        };
276        let mut latest: Option<(u64, bool)> = None;
277        for entity in manager.query_all(|_| true) {
278            let EntityData::Row(row) = &entity.data else {
279                continue;
280            };
281            let Some(named) = &row.named else {
282                continue;
283            };
284            let Some(Value::Text(key)) = named.get("key") else {
285                continue;
286            };
287            if key.as_ref() != PROB_ENCODING_MARKER_KEY {
288                continue;
289            }
290            let is_raw = matches!(
291                named.get("value"),
292                Some(Value::Text(value)) if value.as_ref() == PROB_ENCODING_RAW_V1
293            );
294            let entity_id = entity.id.raw();
295            match latest {
296                Some((existing_id, _)) if existing_id > entity_id => {}
297                _ => latest = Some((entity_id, is_raw)),
298            }
299        }
300        latest.map(|(_, is_raw)| is_raw).unwrap_or(false)
301    }
302
303    fn persist_probabilistic_blob(
304        &self,
305        prefix: &str,
306        name: &str,
307        bytes: &[u8],
308    ) -> RedDBResult<()> {
309        let key = format!("{prefix}{}", hex::encode(name.as_bytes()));
310        self.compact_config_key(&key);
311        self.insert_config_value(&key, Value::Blob(bytes.to_vec()))?;
312        self.persist_probabilistic_encoding_marker();
313        Ok(())
314    }
315
316    fn delete_probabilistic_blob(&self, prefix: &str, name: &str) -> RedDBResult<()> {
317        let key = format!("{prefix}{}", hex::encode(name.as_bytes()));
318        self.compact_config_key(&key);
319        self.insert_config_value(&key, Value::Null)?;
320        Ok(())
321    }
322
323    pub(crate) fn persist_probabilistic_snapshots(&self) -> RedDBResult<()> {
324        let hll_snapshots: Vec<(String, Vec<u8>)> = {
325            let hlls =
326                probabilistic_read(&self.inner.probabilistic.hlls, "probabilistic HLL store");
327            hlls.iter()
328                .map(|(name, hll)| (name.clone(), hll.as_bytes().to_vec()))
329                .collect()
330        };
331        for (name, bytes) in hll_snapshots {
332            self.persist_probabilistic_blob(PROB_HLL_STATE_PREFIX, &name, &bytes)?;
333            self.append_probabilistic_snapshot_delta(PROB_WAL_KIND_HLL, &name, bytes)?;
334        }
335
336        let sketch_snapshots: Vec<(String, Vec<u8>)> = {
337            let sketches = probabilistic_read(
338                &self.inner.probabilistic.sketches,
339                "probabilistic sketch store",
340            );
341            sketches
342                .iter()
343                .map(|(name, sketch)| (name.clone(), sketch.as_bytes()))
344                .collect()
345        };
346        for (name, bytes) in sketch_snapshots {
347            self.persist_probabilistic_blob(PROB_SKETCH_STATE_PREFIX, &name, &bytes)?;
348            self.append_probabilistic_snapshot_delta(PROB_WAL_KIND_SKETCH, &name, bytes)?;
349        }
350
351        let filter_snapshots: Vec<(String, Vec<u8>)> = {
352            let filters = probabilistic_read(
353                &self.inner.probabilistic.filters,
354                "probabilistic filter store",
355            );
356            filters
357                .iter()
358                .map(|(name, filter)| (name.clone(), filter.as_bytes()))
359                .collect()
360        };
361        for (name, bytes) in filter_snapshots {
362            self.persist_probabilistic_blob(PROB_FILTER_STATE_PREFIX, &name, &bytes)?;
363            self.append_probabilistic_snapshot_delta(PROB_WAL_KIND_FILTER, &name, bytes)?;
364        }
365
366        Ok(())
367    }
368
369    fn append_probabilistic_add_delta(
370        &self,
371        kind: u8,
372        name: &str,
373        operands: Vec<Vec<u8>>,
374    ) -> RedDBResult<()> {
375        self.append_probabilistic_delta(kind, PROB_WAL_OP_ADD, name, operands)
376    }
377
378    fn append_probabilistic_snapshot_delta(
379        &self,
380        kind: u8,
381        name: &str,
382        bytes: Vec<u8>,
383    ) -> RedDBResult<()> {
384        self.append_probabilistic_delta(kind, PROB_WAL_OP_SNAPSHOT, name, vec![bytes])
385    }
386
387    fn append_probabilistic_drop_delta(&self, kind: u8, name: &str) -> RedDBResult<()> {
388        self.append_probabilistic_delta(kind, PROB_WAL_OP_DROP, name, Vec::new())
389    }
390
391    fn append_probabilistic_delete_delta(
392        &self,
393        kind: u8,
394        name: &str,
395        operands: Vec<Vec<u8>>,
396    ) -> RedDBResult<()> {
397        self.append_probabilistic_delta(kind, PROB_WAL_OP_DELETE, name, operands)
398    }
399
400    fn append_probabilistic_delta(
401        &self,
402        kind: u8,
403        operation: u8,
404        name: &str,
405        operands: Vec<Vec<u8>>,
406    ) -> RedDBResult<()> {
407        self.inner
408            .db
409            .store()
410            .append_probabilistic_delta_record(kind, operation, name, operands)
411            .map_err(|err| RedDBError::Internal(format!("append probabilistic WAL delta: {err}")))
412    }
413
414    fn apply_replayed_probabilistic_deltas(&self) -> RedDBResult<()> {
415        let deltas = self.inner.db.store().take_replayed_probabilistic_deltas();
416        for (kind, operation, name, operands) in deltas {
417            self.apply_replayed_probabilistic_delta(kind, operation, &name, operands)?;
418        }
419        Ok(())
420    }
421
422    fn apply_replayed_probabilistic_delta(
423        &self,
424        kind: u8,
425        operation: u8,
426        name: &str,
427        operands: Vec<Vec<u8>>,
428    ) -> RedDBResult<()> {
429        match (kind, operation) {
430            (PROB_WAL_KIND_HLL, PROB_WAL_OP_SNAPSHOT) => {
431                let bytes = single_probabilistic_operand(operands, "HLL snapshot")?;
432                let hll = crate::storage::primitives::hyperloglog::HyperLogLog::from_bytes(bytes)
433                    .ok_or_else(|| {
434                    RedDBError::Internal(format!("invalid WAL HLL snapshot for '{name}'"))
435                })?;
436                let mut hlls =
437                    probabilistic_write(&self.inner.probabilistic.hlls, "probabilistic HLL store");
438                hlls.insert(name.to_string(), hll);
439            }
440            (PROB_WAL_KIND_HLL, PROB_WAL_OP_ADD) => {
441                let mut hlls =
442                    probabilistic_write(&self.inner.probabilistic.hlls, "probabilistic HLL store");
443                if let Some(hll) = hlls.get_mut(name) {
444                    for element in operands {
445                        hll.add(&element);
446                    }
447                }
448            }
449            (PROB_WAL_KIND_HLL, PROB_WAL_OP_DROP) => {
450                let mut hlls =
451                    probabilistic_write(&self.inner.probabilistic.hlls, "probabilistic HLL store");
452                hlls.remove(name);
453            }
454            (PROB_WAL_KIND_SKETCH, PROB_WAL_OP_SNAPSHOT) => {
455                let bytes = single_probabilistic_operand(operands, "SKETCH snapshot")?;
456                let sketch =
457                    crate::storage::primitives::count_min_sketch::CountMinSketch::from_bytes(
458                        &bytes,
459                    )
460                    .ok_or_else(|| {
461                        RedDBError::Internal(format!("invalid WAL SKETCH snapshot for '{name}'"))
462                    })?;
463                let mut sketches = probabilistic_write(
464                    &self.inner.probabilistic.sketches,
465                    "probabilistic sketch store",
466                );
467                sketches.insert(name.to_string(), sketch);
468            }
469            (PROB_WAL_KIND_SKETCH, PROB_WAL_OP_ADD) => {
470                let (element, count) = sketch_add_operands(operands)?;
471                let mut sketches = probabilistic_write(
472                    &self.inner.probabilistic.sketches,
473                    "probabilistic sketch store",
474                );
475                if let Some(sketch) = sketches.get_mut(name) {
476                    sketch.add(&element, count);
477                }
478            }
479            (PROB_WAL_KIND_SKETCH, PROB_WAL_OP_DROP) => {
480                let mut sketches = probabilistic_write(
481                    &self.inner.probabilistic.sketches,
482                    "probabilistic sketch store",
483                );
484                sketches.remove(name);
485            }
486            (PROB_WAL_KIND_FILTER, PROB_WAL_OP_SNAPSHOT) => {
487                let bytes = single_probabilistic_operand(operands, "FILTER snapshot")?;
488                let filter =
489                    crate::storage::primitives::cuckoo_filter::CuckooFilter::from_bytes(&bytes)
490                        .ok_or_else(|| {
491                            RedDBError::Internal(format!(
492                                "invalid WAL FILTER snapshot for '{name}'"
493                            ))
494                        })?;
495                let mut filters = probabilistic_write(
496                    &self.inner.probabilistic.filters,
497                    "probabilistic filter store",
498                );
499                filters.insert(name.to_string(), filter);
500            }
501            (PROB_WAL_KIND_FILTER, PROB_WAL_OP_ADD) => {
502                let element = single_probabilistic_operand(operands, "FILTER ADD")?;
503                let mut filters = probabilistic_write(
504                    &self.inner.probabilistic.filters,
505                    "probabilistic filter store",
506                );
507                if let Some(filter) = filters.get_mut(name) {
508                    let _ = filter.insert(&element);
509                }
510            }
511            (PROB_WAL_KIND_FILTER, PROB_WAL_OP_DELETE) => {
512                let element = single_probabilistic_operand(operands, "FILTER DELETE")?;
513                let mut filters = probabilistic_write(
514                    &self.inner.probabilistic.filters,
515                    "probabilistic filter store",
516                );
517                if let Some(filter) = filters.get_mut(name) {
518                    let _ = filter.delete(&element);
519                }
520            }
521            (PROB_WAL_KIND_FILTER, PROB_WAL_OP_DROP) => {
522                let mut filters = probabilistic_write(
523                    &self.inner.probabilistic.filters,
524                    "probabilistic filter store",
525                );
526                filters.remove(name);
527            }
528            _ => {
529                return Err(RedDBError::Internal(format!(
530                    "unknown probabilistic WAL delta kind={kind} operation={operation}"
531                )));
532            }
533        }
534        Ok(())
535    }
536
537    fn persist_probabilistic_encoding_marker(&self) {
538        self.compact_config_key(PROB_ENCODING_MARKER_KEY);
539        let _ = self.insert_config_value(
540            PROB_ENCODING_MARKER_KEY,
541            Value::text(PROB_ENCODING_RAW_V1.to_string()),
542        );
543    }
544
545    fn compact_config_key(&self, key: &str) {
546        let store = self.inner.db.store();
547        let Some(manager) = store.get_collection("red_config") else {
548            return;
549        };
550        let ids: Vec<EntityId> = manager
551            .query_all(|_| true)
552            .into_iter()
553            .filter_map(|entity| {
554                let EntityData::Row(row) = &entity.data else {
555                    return None;
556                };
557                let named = row.named.as_ref()?;
558                let Some(Value::Text(candidate)) = named.get("key") else {
559                    return None;
560                };
561                (candidate.as_ref() == key).then_some(entity.id)
562            })
563            .collect();
564        for id in ids {
565            let _ = store.delete("red_config", id);
566        }
567    }
568
569    fn insert_config_value(&self, key: &str, value: Value) -> RedDBResult<()> {
570        let store = self.inner.db.store();
571        let _ = store.get_or_create_collection("red_config");
572        let entity = UnifiedEntity::new(
573            EntityId::new(0),
574            EntityKind::TableRow {
575                table: std::sync::Arc::from("red_config"),
576                row_id: 0,
577            },
578            EntityData::Row(crate::storage::RowData {
579                columns: Vec::new(),
580                named: Some(
581                    [
582                        ("key".to_string(), Value::text(key.to_string())),
583                        ("value".to_string(), value),
584                    ]
585                    .into_iter()
586                    .collect(),
587                ),
588                schema: None,
589            }),
590        );
591        store
592            .insert_auto("red_config", entity)
593            .map_err(|err| RedDBError::Internal(err.to_string()))?;
594        Ok(())
595    }
596
597    fn create_probabilistic_catalog_entry(
598        &self,
599        name: &str,
600        model: crate::catalog::CollectionModel,
601    ) -> RedDBResult<()> {
602        let store = self.inner.db.store();
603        store
604            .create_collection(name)
605            .map_err(|err| RedDBError::Internal(err.to_string()))?;
606        self.inner
607            .db
608            .save_collection_contract(probabilistic_collection_contract(name, model))
609            .map_err(|err| RedDBError::Internal(err.to_string()))?;
610        if let Some(tenant_id) = crate::runtime::impl_core::current_tenant() {
611            store.set_config_tree(
612                &format!("red.collection_tenants.{name}"),
613                &crate::serde_json::Value::String(tenant_id),
614            );
615        }
616        self.inner
617            .db
618            .persist_metadata()
619            .map_err(|err| RedDBError::Internal(err.to_string()))?;
620        self.invalidate_result_cache();
621        Ok(())
622    }
623
624    fn drop_probabilistic_catalog_entry(&self, name: &str) -> RedDBResult<()> {
625        let store = self.inner.db.store();
626        if store.get_collection(name).is_some() {
627            store
628                .drop_collection(name)
629                .map_err(|err| RedDBError::Internal(err.to_string()))?;
630        }
631        self.inner
632            .db
633            .remove_collection_contract(name)
634            .map_err(|err| RedDBError::Internal(err.to_string()))?;
635        self.inner
636            .db
637            .persist_metadata()
638            .map_err(|err| RedDBError::Internal(err.to_string()))?;
639        self.invalidate_result_cache();
640        Ok(())
641    }
642
643    pub(crate) fn execute_probabilistic_select(
644        &self,
645        query: &TableQuery,
646    ) -> RedDBResult<Option<UnifiedResult>> {
647        let projections = crate::storage::query::sql_lowering::effective_table_projections(query);
648        let mut read_projections = Vec::new();
649        for projection in &projections {
650            if let Some(read_projection) =
651                parse_probabilistic_read_projection(projection, read_projections.len())?
652            {
653                read_projections.push(read_projection);
654            }
655        }
656
657        let Some(actual_model) = self
658            .inner
659            .db
660            .collection_contract(&query.table)
661            .map(|contract| contract.declared_model)
662        else {
663            return if read_projections.is_empty() {
664                Ok(None)
665            } else {
666                Err(RedDBError::NotFound(format!(
667                    "probabilistic collection '{}' not found",
668                    query.table
669                )))
670            };
671        };
672
673        let is_probabilistic_model = matches!(
674            actual_model,
675            crate::catalog::CollectionModel::Hll
676                | crate::catalog::CollectionModel::Sketch
677                | crate::catalog::CollectionModel::Filter
678        );
679        if read_projections.is_empty() {
680            return if is_probabilistic_model {
681                Err(RedDBError::Query(format!(
682                    "probabilistic collection '{}' supports SELECT CARDINALITY, FREQ(...), or CONTAINS(...) read forms",
683                    query.table
684                )))
685            } else {
686                Ok(None)
687            };
688        }
689
690        validate_probabilistic_read_model(&query.table, actual_model, &read_projections)?;
691        let (columns, record) =
692            self.materialize_probabilistic_select_row(&query.table, &read_projections)?;
693        let mut result = UnifiedResult::with_columns(columns);
694        if probabilistic_select_row_visible(self, query, &record) {
695            result.push(record);
696        }
697        Ok(Some(result))
698    }
699
700    pub fn execute_probabilistic_command(
701        &self,
702        raw_query: &str,
703        cmd: &ProbabilisticCommand,
704    ) -> RedDBResult<RuntimeQueryResult> {
705        // Mixed read/write surface: count/info/check are read-side and
706        // must remain available on read-only replicas; create/add/
707        // merge/delete/drop are mutations and must go through the gate.
708        let is_mutation = matches!(
709            cmd,
710            ProbabilisticCommand::CreateHll { .. }
711                | ProbabilisticCommand::HllAdd { .. }
712                | ProbabilisticCommand::HllMerge { .. }
713                | ProbabilisticCommand::DropHll { .. }
714                | ProbabilisticCommand::CreateSketch { .. }
715                | ProbabilisticCommand::SketchAdd { .. }
716                | ProbabilisticCommand::SketchMerge { .. }
717                | ProbabilisticCommand::DropSketch { .. }
718                | ProbabilisticCommand::CreateFilter { .. }
719                | ProbabilisticCommand::FilterAdd { .. }
720                | ProbabilisticCommand::FilterDelete { .. }
721                | ProbabilisticCommand::DropFilter { .. }
722        );
723        if is_mutation {
724            self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
725        }
726        match cmd {
727            // ── HyperLogLog ──────────────────────────────────────────
728            ProbabilisticCommand::CreateHll {
729                name,
730                precision,
731                if_not_exists,
732            } => {
733                let mut hlls =
734                    probabilistic_write(&self.inner.probabilistic.hlls, "probabilistic HLL store");
735                if hlls.contains_key(name) {
736                    if *if_not_exists {
737                        return Ok(RuntimeQueryResult::ok_message(
738                            raw_query.to_string(),
739                            &format!("HLL '{}' already exists", name),
740                            "create",
741                        ));
742                    }
743                    return Err(RedDBError::Query(format!("HLL '{}' already exists", name)));
744                }
745                let hll = crate::storage::primitives::hyperloglog::HyperLogLog::with_precision(
746                    *precision,
747                )
748                .ok_or_else(|| {
749                    RedDBError::Query(format!(
750                        "HLL precision must be between 4 and 18, got {precision}"
751                    ))
752                })?;
753                self.create_probabilistic_catalog_entry(
754                    name,
755                    crate::catalog::CollectionModel::Hll,
756                )?;
757                self.persist_probabilistic_blob(PROB_HLL_STATE_PREFIX, name, hll.as_bytes())?;
758                self.append_probabilistic_snapshot_delta(
759                    PROB_WAL_KIND_HLL,
760                    name,
761                    hll.as_bytes().to_vec(),
762                )?;
763                hlls.insert(name.clone(), hll);
764                Ok(RuntimeQueryResult::ok_message(
765                    raw_query.to_string(),
766                    &format!("HLL '{}' created", name),
767                    "create",
768                ))
769            }
770            ProbabilisticCommand::HllAdd { name, elements } => {
771                let mut hlls =
772                    probabilistic_write(&self.inner.probabilistic.hlls, "probabilistic HLL store");
773                let hll = hlls
774                    .get_mut(name)
775                    .ok_or_else(|| RedDBError::NotFound(format!("HLL '{}' not found", name)))?;
776                for elem in elements {
777                    hll.add(elem.as_bytes());
778                }
779                self.append_probabilistic_add_delta(
780                    PROB_WAL_KIND_HLL,
781                    name,
782                    elements
783                        .iter()
784                        .map(|element| element.as_bytes().to_vec())
785                        .collect(),
786                )?;
787                Ok(RuntimeQueryResult::ok_message(
788                    raw_query.to_string(),
789                    &format!("{} element(s) added to HLL '{}'", elements.len(), name),
790                    "insert",
791                ))
792            }
793            ProbabilisticCommand::HllCount { names } => {
794                let hlls =
795                    probabilistic_read(&self.inner.probabilistic.hlls, "probabilistic HLL store");
796                if names.is_empty() {
797                    return Err(RedDBError::Query(
798                        "HLL COUNT requires at least one HLL name".to_string(),
799                    ));
800                }
801                if names.len() == 1 {
802                    let hll = hlls.get(&names[0]).ok_or_else(|| {
803                        RedDBError::NotFound(format!("HLL '{}' not found", names[0]))
804                    })?;
805                    let count = hll.count();
806                    let mut result = UnifiedResult::with_columns(vec!["count".into()]);
807                    let mut record = UnifiedRecord::new();
808                    record.set("count", Value::UnsignedInteger(count));
809                    result.push(record);
810                    Ok(RuntimeQueryResult {
811                        query: raw_query.to_string(),
812                        mode: QueryMode::Sql,
813                        statement: "hll_count",
814                        engine: "runtime-probabilistic",
815                        result,
816                        affected_rows: 0,
817                        statement_type: "select",
818                        bookmark: None,
819                        notice: None,
820                    })
821                } else {
822                    // Multi-HLL count = union count
823                    let first_name = &names[0];
824                    let first = hlls.get(first_name).ok_or_else(|| {
825                        RedDBError::NotFound(format!("HLL '{}' not found", first_name))
826                    })?;
827                    let expected_precision = first.precision();
828                    let mut merged =
829                        crate::storage::primitives::hyperloglog::HyperLogLog::with_precision(
830                            expected_precision,
831                        )
832                        .expect("loaded HLL precision is valid");
833                    for name in names {
834                        let hll = hlls.get(name).ok_or_else(|| {
835                            RedDBError::NotFound(format!("HLL '{}' not found", name))
836                        })?;
837                        if hll.precision() != expected_precision {
838                            return Err(hll_precision_mismatch_error(
839                                "COUNT",
840                                first_name,
841                                expected_precision,
842                                name,
843                                hll.precision(),
844                            ));
845                        }
846                        merged.merge(hll);
847                    }
848                    let count = merged.count();
849                    let mut result = UnifiedResult::with_columns(vec!["count".into()]);
850                    let mut record = UnifiedRecord::new();
851                    record.set("count", Value::UnsignedInteger(count));
852                    result.push(record);
853                    Ok(RuntimeQueryResult {
854                        query: raw_query.to_string(),
855                        mode: QueryMode::Sql,
856                        statement: "hll_count",
857                        engine: "runtime-probabilistic",
858                        result,
859                        affected_rows: 0,
860                        statement_type: "select",
861                        bookmark: None,
862                        notice: None,
863                    })
864                }
865            }
866            ProbabilisticCommand::HllMerge { dest, sources } => {
867                let mut hlls =
868                    probabilistic_write(&self.inner.probabilistic.hlls, "probabilistic HLL store");
869                let first_src = sources.first().ok_or_else(|| {
870                    RedDBError::Query("HLL MERGE requires at least one source HLL".to_string())
871                })?;
872                let first = hlls.get(first_src).ok_or_else(|| {
873                    RedDBError::NotFound(format!("HLL '{}' not found", first_src))
874                })?;
875                let expected_precision = first.precision();
876                let mut merged =
877                    crate::storage::primitives::hyperloglog::HyperLogLog::with_precision(
878                        expected_precision,
879                    )
880                    .expect("loaded HLL precision is valid");
881                for src in sources {
882                    let hll = hlls
883                        .get(src)
884                        .ok_or_else(|| RedDBError::NotFound(format!("HLL '{}' not found", src)))?;
885                    if hll.precision() != expected_precision {
886                        return Err(hll_precision_mismatch_error(
887                            "MERGE",
888                            first_src,
889                            expected_precision,
890                            src,
891                            hll.precision(),
892                        ));
893                    }
894                    merged.merge(hll);
895                }
896                self.persist_probabilistic_blob(PROB_HLL_STATE_PREFIX, dest, merged.as_bytes())?;
897                self.append_probabilistic_snapshot_delta(
898                    PROB_WAL_KIND_HLL,
899                    dest,
900                    merged.as_bytes().to_vec(),
901                )?;
902                hlls.insert(dest.clone(), merged);
903                Ok(RuntimeQueryResult::ok_message(
904                    raw_query.to_string(),
905                    &format!(
906                        "HLL '{}' created from merge of {}",
907                        dest,
908                        sources.join(", ")
909                    ),
910                    "create",
911                ))
912            }
913            ProbabilisticCommand::HllInfo { name } => {
914                let hlls =
915                    probabilistic_read(&self.inner.probabilistic.hlls, "probabilistic HLL store");
916                let hll = hlls
917                    .get(name)
918                    .ok_or_else(|| RedDBError::NotFound(format!("HLL '{}' not found", name)))?;
919                let mut result = UnifiedResult::with_columns(vec![
920                    "name".into(),
921                    "precision".into(),
922                    "count".into(),
923                    "memory_bytes".into(),
924                ]);
925                let mut record = UnifiedRecord::new();
926                record.set("name", Value::text(name.clone()));
927                record.set("precision", Value::UnsignedInteger(hll.precision() as u64));
928                record.set("count", Value::UnsignedInteger(hll.count()));
929                record.set(
930                    "memory_bytes",
931                    Value::UnsignedInteger(hll.memory_bytes() as u64),
932                );
933                result.push(record);
934                Ok(RuntimeQueryResult {
935                    query: raw_query.to_string(),
936                    mode: QueryMode::Sql,
937                    statement: "hll_info",
938                    engine: "runtime-probabilistic",
939                    result,
940                    affected_rows: 0,
941                    statement_type: "select",
942                    bookmark: None,
943                    notice: None,
944                })
945            }
946            ProbabilisticCommand::DropHll { name, if_exists } => {
947                let mut hlls =
948                    probabilistic_write(&self.inner.probabilistic.hlls, "probabilistic HLL store");
949                if hlls.remove(name).is_none() {
950                    if *if_exists {
951                        return Ok(RuntimeQueryResult::ok_message(
952                            raw_query.to_string(),
953                            &format!("HLL '{}' does not exist", name),
954                            "drop",
955                        ));
956                    }
957                    return Err(RedDBError::NotFound(format!("HLL '{}' not found", name)));
958                }
959                self.drop_probabilistic_catalog_entry(name)?;
960                self.delete_probabilistic_blob(PROB_HLL_STATE_PREFIX, name)?;
961                self.append_probabilistic_drop_delta(PROB_WAL_KIND_HLL, name)?;
962                Ok(RuntimeQueryResult::ok_message(
963                    raw_query.to_string(),
964                    &format!("HLL '{}' dropped", name),
965                    "drop",
966                ))
967            }
968
969            // ── Count-Min Sketch ───────────────────────────────────────
970            ProbabilisticCommand::CreateSketch {
971                name,
972                width,
973                depth,
974                if_not_exists,
975            } => {
976                let mut sketches = probabilistic_write(
977                    &self.inner.probabilistic.sketches,
978                    "probabilistic sketch store",
979                );
980                if sketches.contains_key(name) {
981                    if *if_not_exists {
982                        return Ok(RuntimeQueryResult::ok_message(
983                            raw_query.to_string(),
984                            &format!("SKETCH '{}' already exists", name),
985                            "create",
986                        ));
987                    }
988                    return Err(RedDBError::Query(format!(
989                        "SKETCH '{}' already exists",
990                        name
991                    )));
992                }
993                self.create_probabilistic_catalog_entry(
994                    name,
995                    crate::catalog::CollectionModel::Sketch,
996                )?;
997                let sketch = crate::storage::primitives::count_min_sketch::CountMinSketch::new(
998                    *width, *depth,
999                );
1000                self.persist_probabilistic_blob(
1001                    PROB_SKETCH_STATE_PREFIX,
1002                    name,
1003                    &sketch.as_bytes(),
1004                )?;
1005                self.append_probabilistic_snapshot_delta(
1006                    PROB_WAL_KIND_SKETCH,
1007                    name,
1008                    sketch.as_bytes(),
1009                )?;
1010                sketches.insert(name.clone(), sketch);
1011                Ok(RuntimeQueryResult::ok_message(
1012                    raw_query.to_string(),
1013                    &format!(
1014                        "SKETCH '{}' created (width={}, depth={})",
1015                        name, width, depth
1016                    ),
1017                    "create",
1018                ))
1019            }
1020            ProbabilisticCommand::SketchAdd {
1021                name,
1022                element,
1023                count,
1024            } => {
1025                let mut sketches = probabilistic_write(
1026                    &self.inner.probabilistic.sketches,
1027                    "probabilistic sketch store",
1028                );
1029                let sketch = sketches
1030                    .get_mut(name)
1031                    .ok_or_else(|| RedDBError::NotFound(format!("SKETCH '{}' not found", name)))?;
1032                sketch.add(element.as_bytes(), *count);
1033                self.append_probabilistic_add_delta(
1034                    PROB_WAL_KIND_SKETCH,
1035                    name,
1036                    vec![element.as_bytes().to_vec(), count.to_le_bytes().to_vec()],
1037                )?;
1038                Ok(RuntimeQueryResult::ok_message(
1039                    raw_query.to_string(),
1040                    &format!("added {} to SKETCH '{}'", count, name),
1041                    "insert",
1042                ))
1043            }
1044            ProbabilisticCommand::SketchCount { name, element } => {
1045                let sketches = probabilistic_read(
1046                    &self.inner.probabilistic.sketches,
1047                    "probabilistic sketch store",
1048                );
1049                let sketch = sketches
1050                    .get(name)
1051                    .ok_or_else(|| RedDBError::NotFound(format!("SKETCH '{}' not found", name)))?;
1052                let estimate = sketch.estimate(element.as_bytes());
1053                let mut result = UnifiedResult::with_columns(vec!["estimate".into()]);
1054                let mut record = UnifiedRecord::new();
1055                record.set("estimate", Value::UnsignedInteger(estimate));
1056                result.push(record);
1057                Ok(RuntimeQueryResult {
1058                    query: raw_query.to_string(),
1059                    mode: QueryMode::Sql,
1060                    statement: "sketch_count",
1061                    engine: "runtime-probabilistic",
1062                    result,
1063                    affected_rows: 0,
1064                    statement_type: "select",
1065                    bookmark: None,
1066                    notice: None,
1067                })
1068            }
1069            ProbabilisticCommand::SketchMerge { dest, sources } => {
1070                let mut sketches = probabilistic_write(
1071                    &self.inner.probabilistic.sketches,
1072                    "probabilistic sketch store",
1073                );
1074                let first_src = sketches.get(&sources[0]).ok_or_else(|| {
1075                    RedDBError::NotFound(format!("SKETCH '{}' not found", sources[0]))
1076                })?;
1077                let mut merged = crate::storage::primitives::count_min_sketch::CountMinSketch::new(
1078                    first_src.width(),
1079                    first_src.depth(),
1080                );
1081                for src in sources {
1082                    let sketch = sketches.get(src).ok_or_else(|| {
1083                        RedDBError::NotFound(format!("SKETCH '{}' not found", src))
1084                    })?;
1085                    if !merged.merge(sketch) {
1086                        return Err(RedDBError::Query(format!(
1087                            "SKETCH '{}' has incompatible dimensions",
1088                            src
1089                        )));
1090                    }
1091                }
1092                self.persist_probabilistic_blob(
1093                    PROB_SKETCH_STATE_PREFIX,
1094                    dest,
1095                    &merged.as_bytes(),
1096                )?;
1097                self.append_probabilistic_snapshot_delta(
1098                    PROB_WAL_KIND_SKETCH,
1099                    dest,
1100                    merged.as_bytes(),
1101                )?;
1102                sketches.insert(dest.clone(), merged);
1103                Ok(RuntimeQueryResult::ok_message(
1104                    raw_query.to_string(),
1105                    &format!(
1106                        "SKETCH '{}' created from merge of {}",
1107                        dest,
1108                        sources.join(", ")
1109                    ),
1110                    "create",
1111                ))
1112            }
1113            ProbabilisticCommand::SketchInfo { name } => {
1114                let sketches = probabilistic_read(
1115                    &self.inner.probabilistic.sketches,
1116                    "probabilistic sketch store",
1117                );
1118                let sketch = sketches
1119                    .get(name)
1120                    .ok_or_else(|| RedDBError::NotFound(format!("SKETCH '{}' not found", name)))?;
1121                let mut result = UnifiedResult::with_columns(vec![
1122                    "name".into(),
1123                    "width".into(),
1124                    "depth".into(),
1125                    "total".into(),
1126                    "memory_bytes".into(),
1127                ]);
1128                let mut record = UnifiedRecord::new();
1129                record.set("name", Value::text(name.clone()));
1130                record.set("width", Value::UnsignedInteger(sketch.width() as u64));
1131                record.set("depth", Value::UnsignedInteger(sketch.depth() as u64));
1132                record.set("total", Value::UnsignedInteger(sketch.total()));
1133                record.set(
1134                    "memory_bytes",
1135                    Value::UnsignedInteger(sketch.memory_bytes() as u64),
1136                );
1137                result.push(record);
1138                Ok(RuntimeQueryResult {
1139                    query: raw_query.to_string(),
1140                    mode: QueryMode::Sql,
1141                    statement: "sketch_info",
1142                    engine: "runtime-probabilistic",
1143                    result,
1144                    affected_rows: 0,
1145                    statement_type: "select",
1146                    bookmark: None,
1147                    notice: None,
1148                })
1149            }
1150            ProbabilisticCommand::DropSketch { name, if_exists } => {
1151                let mut sketches = probabilistic_write(
1152                    &self.inner.probabilistic.sketches,
1153                    "probabilistic sketch store",
1154                );
1155                if sketches.remove(name).is_none() {
1156                    if *if_exists {
1157                        return Ok(RuntimeQueryResult::ok_message(
1158                            raw_query.to_string(),
1159                            &format!("SKETCH '{}' does not exist", name),
1160                            "drop",
1161                        ));
1162                    }
1163                    return Err(RedDBError::NotFound(format!("SKETCH '{}' not found", name)));
1164                }
1165                self.drop_probabilistic_catalog_entry(name)?;
1166                self.delete_probabilistic_blob(PROB_SKETCH_STATE_PREFIX, name)?;
1167                self.append_probabilistic_drop_delta(PROB_WAL_KIND_SKETCH, name)?;
1168                Ok(RuntimeQueryResult::ok_message(
1169                    raw_query.to_string(),
1170                    &format!("SKETCH '{}' dropped", name),
1171                    "drop",
1172                ))
1173            }
1174
1175            // ── Cuckoo Filter ─────────────────────────────────────────
1176            ProbabilisticCommand::CreateFilter {
1177                name,
1178                capacity,
1179                if_not_exists,
1180            } => {
1181                let mut filters = probabilistic_write(
1182                    &self.inner.probabilistic.filters,
1183                    "probabilistic filter store",
1184                );
1185                if filters.contains_key(name) {
1186                    if *if_not_exists {
1187                        return Ok(RuntimeQueryResult::ok_message(
1188                            raw_query.to_string(),
1189                            &format!("FILTER '{}' already exists", name),
1190                            "create",
1191                        ));
1192                    }
1193                    return Err(RedDBError::Query(format!(
1194                        "FILTER '{}' already exists",
1195                        name
1196                    )));
1197                }
1198                self.create_probabilistic_catalog_entry(
1199                    name,
1200                    crate::catalog::CollectionModel::Filter,
1201                )?;
1202                let filter =
1203                    crate::storage::primitives::cuckoo_filter::CuckooFilter::new(*capacity);
1204                self.persist_probabilistic_blob(
1205                    PROB_FILTER_STATE_PREFIX,
1206                    name,
1207                    &filter.as_bytes(),
1208                )?;
1209                self.append_probabilistic_snapshot_delta(
1210                    PROB_WAL_KIND_FILTER,
1211                    name,
1212                    filter.as_bytes(),
1213                )?;
1214                filters.insert(name.clone(), filter);
1215                Ok(RuntimeQueryResult::ok_message(
1216                    raw_query.to_string(),
1217                    &format!("FILTER '{}' created (capacity={})", name, capacity),
1218                    "create",
1219                ))
1220            }
1221            ProbabilisticCommand::FilterAdd { name, element } => {
1222                let mut filters = probabilistic_write(
1223                    &self.inner.probabilistic.filters,
1224                    "probabilistic filter store",
1225                );
1226                let filter = filters
1227                    .get_mut(name)
1228                    .ok_or_else(|| RedDBError::NotFound(format!("FILTER '{}' not found", name)))?;
1229                if !filter.insert(element.as_bytes()) {
1230                    return Err(RedDBError::Query(format!("FILTER '{}' is full", name)));
1231                }
1232                self.append_probabilistic_add_delta(
1233                    PROB_WAL_KIND_FILTER,
1234                    name,
1235                    vec![element.as_bytes().to_vec()],
1236                )?;
1237                Ok(RuntimeQueryResult::ok_message(
1238                    raw_query.to_string(),
1239                    &format!("element added to FILTER '{}'", name),
1240                    "insert",
1241                ))
1242            }
1243            ProbabilisticCommand::FilterCheck { name, element } => {
1244                let filters = probabilistic_read(
1245                    &self.inner.probabilistic.filters,
1246                    "probabilistic filter store",
1247                );
1248                let filter = filters
1249                    .get(name)
1250                    .ok_or_else(|| RedDBError::NotFound(format!("FILTER '{}' not found", name)))?;
1251                let exists = filter.contains(element.as_bytes());
1252                let mut result = UnifiedResult::with_columns(vec!["exists".into()]);
1253                let mut record = UnifiedRecord::new();
1254                record.set("exists", Value::Boolean(exists));
1255                result.push(record);
1256                Ok(RuntimeQueryResult {
1257                    query: raw_query.to_string(),
1258                    mode: QueryMode::Sql,
1259                    statement: "filter_check",
1260                    engine: "runtime-probabilistic",
1261                    result,
1262                    affected_rows: 0,
1263                    statement_type: "select",
1264                    bookmark: None,
1265                    notice: None,
1266                })
1267            }
1268            ProbabilisticCommand::FilterDelete { name, element } => {
1269                let mut filters = probabilistic_write(
1270                    &self.inner.probabilistic.filters,
1271                    "probabilistic filter store",
1272                );
1273                let filter = filters
1274                    .get_mut(name)
1275                    .ok_or_else(|| RedDBError::NotFound(format!("FILTER '{}' not found", name)))?;
1276                let removed = filter.delete(element.as_bytes());
1277                self.append_probabilistic_delete_delta(
1278                    PROB_WAL_KIND_FILTER,
1279                    name,
1280                    vec![element.as_bytes().to_vec()],
1281                )?;
1282                Ok(RuntimeQueryResult::ok_message(
1283                    raw_query.to_string(),
1284                    &format!(
1285                        "element {} from FILTER '{}'",
1286                        if removed { "deleted" } else { "not found in" },
1287                        name
1288                    ),
1289                    "delete",
1290                ))
1291            }
1292            ProbabilisticCommand::FilterCount { name } => {
1293                let filters = probabilistic_read(
1294                    &self.inner.probabilistic.filters,
1295                    "probabilistic filter store",
1296                );
1297                let filter = filters
1298                    .get(name)
1299                    .ok_or_else(|| RedDBError::NotFound(format!("FILTER '{}' not found", name)))?;
1300                let mut result = UnifiedResult::with_columns(vec!["count".into()]);
1301                let mut record = UnifiedRecord::new();
1302                record.set("count", Value::UnsignedInteger(filter.count() as u64));
1303                result.push(record);
1304                Ok(RuntimeQueryResult {
1305                    query: raw_query.to_string(),
1306                    mode: QueryMode::Sql,
1307                    statement: "filter_count",
1308                    engine: "runtime-probabilistic",
1309                    result,
1310                    affected_rows: 0,
1311                    statement_type: "select",
1312                    bookmark: None,
1313                    notice: None,
1314                })
1315            }
1316            ProbabilisticCommand::FilterInfo { name } => {
1317                let filters = probabilistic_read(
1318                    &self.inner.probabilistic.filters,
1319                    "probabilistic filter store",
1320                );
1321                let filter = filters
1322                    .get(name)
1323                    .ok_or_else(|| RedDBError::NotFound(format!("FILTER '{}' not found", name)))?;
1324                let mut result = UnifiedResult::with_columns(vec![
1325                    "name".into(),
1326                    "count".into(),
1327                    "load_factor".into(),
1328                    "memory_bytes".into(),
1329                ]);
1330                let mut record = UnifiedRecord::new();
1331                record.set("name", Value::text(name.clone()));
1332                record.set("count", Value::UnsignedInteger(filter.count() as u64));
1333                record.set("load_factor", Value::Float(filter.load_factor()));
1334                record.set(
1335                    "memory_bytes",
1336                    Value::UnsignedInteger(filter.memory_bytes() as u64),
1337                );
1338                result.push(record);
1339                Ok(RuntimeQueryResult {
1340                    query: raw_query.to_string(),
1341                    mode: QueryMode::Sql,
1342                    statement: "filter_info",
1343                    engine: "runtime-probabilistic",
1344                    result,
1345                    affected_rows: 0,
1346                    statement_type: "select",
1347                    bookmark: None,
1348                    notice: None,
1349                })
1350            }
1351            ProbabilisticCommand::DropFilter { name, if_exists } => {
1352                let mut filters = probabilistic_write(
1353                    &self.inner.probabilistic.filters,
1354                    "probabilistic filter store",
1355                );
1356                if filters.remove(name).is_none() {
1357                    if *if_exists {
1358                        return Ok(RuntimeQueryResult::ok_message(
1359                            raw_query.to_string(),
1360                            &format!("FILTER '{}' does not exist", name),
1361                            "drop",
1362                        ));
1363                    }
1364                    return Err(RedDBError::NotFound(format!("FILTER '{}' not found", name)));
1365                }
1366                self.drop_probabilistic_catalog_entry(name)?;
1367                self.delete_probabilistic_blob(PROB_FILTER_STATE_PREFIX, name)?;
1368                self.append_probabilistic_drop_delta(PROB_WAL_KIND_FILTER, name)?;
1369                Ok(RuntimeQueryResult::ok_message(
1370                    raw_query.to_string(),
1371                    &format!("FILTER '{}' dropped", name),
1372                    "drop",
1373                ))
1374            }
1375        }
1376    }
1377}
1378
1379fn parse_probabilistic_read_projection(
1380    projection: &Projection,
1381    index: usize,
1382) -> RedDBResult<Option<ProbabilisticReadProjection>> {
1383    if let Some(column) = projection_unqualified_column(projection) {
1384        if column.eq_ignore_ascii_case("CARDINALITY") {
1385            return Ok(Some(ProbabilisticReadProjection::Cardinality {
1386                label: probabilistic_projection_label(projection, "cardinality", index),
1387            }));
1388        }
1389    }
1390
1391    let Some((function, args)) = projection_function(projection) else {
1392        return Ok(None);
1393    };
1394    if function.eq_ignore_ascii_case("FREQ") {
1395        let element = projection_single_text_arg(function, args)?;
1396        return Ok(Some(ProbabilisticReadProjection::Freq {
1397            element,
1398            label: probabilistic_projection_label(projection, "freq", index),
1399        }));
1400    }
1401    if function.eq_ignore_ascii_case("CONTAINS") {
1402        let element = projection_single_text_arg(function, args)?;
1403        return Ok(Some(ProbabilisticReadProjection::Contains {
1404            element,
1405            label: probabilistic_projection_label(projection, "contains", index),
1406        }));
1407    }
1408
1409    Ok(None)
1410}
1411
1412fn single_probabilistic_operand(mut operands: Vec<Vec<u8>>, context: &str) -> RedDBResult<Vec<u8>> {
1413    if operands.len() != 1 {
1414        return Err(RedDBError::Internal(format!(
1415            "{context} WAL delta expected one operand, got {}",
1416            operands.len()
1417        )));
1418    }
1419    Ok(operands.remove(0))
1420}
1421
1422fn sketch_add_operands(mut operands: Vec<Vec<u8>>) -> RedDBResult<(Vec<u8>, u64)> {
1423    if operands.len() != 2 {
1424        return Err(RedDBError::Internal(format!(
1425            "SKETCH ADD WAL delta expected two operands, got {}",
1426            operands.len()
1427        )));
1428    }
1429    let count_bytes = operands.remove(1);
1430    let count_array: [u8; 8] = count_bytes.as_slice().try_into().map_err(|_| {
1431        RedDBError::Internal(format!(
1432            "SKETCH ADD WAL delta count expected 8 bytes, got {}",
1433            count_bytes.len()
1434        ))
1435    })?;
1436    Ok((operands.remove(0), u64::from_le_bytes(count_array)))
1437}
1438
1439fn validate_probabilistic_read_model(
1440    collection: &str,
1441    actual_model: crate::catalog::CollectionModel,
1442    projections: &[ProbabilisticReadProjection],
1443) -> RedDBResult<()> {
1444    for projection in projections {
1445        let expected_model = match projection {
1446            ProbabilisticReadProjection::Cardinality { .. } => crate::catalog::CollectionModel::Hll,
1447            ProbabilisticReadProjection::Freq { .. } => crate::catalog::CollectionModel::Sketch,
1448            ProbabilisticReadProjection::Contains { .. } => crate::catalog::CollectionModel::Filter,
1449        };
1450        if actual_model != expected_model {
1451            return Err(RedDBError::Query(format!(
1452                "{} is only supported for {} collections; '{}' is {}",
1453                probabilistic_projection_form(projection),
1454                crate::runtime::ddl::polymorphic_resolver::model_name(expected_model),
1455                collection,
1456                crate::runtime::ddl::polymorphic_resolver::model_name(actual_model)
1457            )));
1458        }
1459    }
1460    Ok(())
1461}
1462
1463impl RedDBRuntime {
1464    fn materialize_probabilistic_select_row(
1465        &self,
1466        collection: &str,
1467        projections: &[ProbabilisticReadProjection],
1468    ) -> RedDBResult<(Vec<String>, UnifiedRecord)> {
1469        let mut columns = Vec::with_capacity(projections.len());
1470        let mut record = UnifiedRecord::new();
1471        for projection in projections {
1472            match projection {
1473                ProbabilisticReadProjection::Cardinality { label } => {
1474                    let hlls = probabilistic_read(
1475                        &self.inner.probabilistic.hlls,
1476                        "probabilistic HLL store",
1477                    );
1478                    let hll = hlls.get(collection).ok_or_else(|| {
1479                        RedDBError::NotFound(format!("HLL '{}' not found", collection))
1480                    })?;
1481                    columns.push(label.clone());
1482                    record.set(label, Value::UnsignedInteger(hll.count()));
1483                }
1484                ProbabilisticReadProjection::Freq { element, label } => {
1485                    let sketches = probabilistic_read(
1486                        &self.inner.probabilistic.sketches,
1487                        "probabilistic sketch store",
1488                    );
1489                    let sketch = sketches.get(collection).ok_or_else(|| {
1490                        RedDBError::NotFound(format!("SKETCH '{}' not found", collection))
1491                    })?;
1492                    columns.push(label.clone());
1493                    record.set(
1494                        label,
1495                        Value::UnsignedInteger(sketch.estimate(element.as_bytes())),
1496                    );
1497                }
1498                ProbabilisticReadProjection::Contains { element, label } => {
1499                    let filters = probabilistic_read(
1500                        &self.inner.probabilistic.filters,
1501                        "probabilistic filter store",
1502                    );
1503                    let filter = filters.get(collection).ok_or_else(|| {
1504                        RedDBError::NotFound(format!("FILTER '{}' not found", collection))
1505                    })?;
1506                    columns.push(label.clone());
1507                    record.set(label, Value::Boolean(filter.contains(element.as_bytes())));
1508                }
1509            }
1510        }
1511        Ok((columns, record))
1512    }
1513}
1514
1515fn probabilistic_select_row_visible(
1516    runtime: &RedDBRuntime,
1517    query: &TableQuery,
1518    record: &UnifiedRecord,
1519) -> bool {
1520    if query.limit == Some(0) || query.offset.is_some_and(|offset| offset > 0) {
1521        return false;
1522    }
1523    let table_name = query.table.as_str();
1524    let table_alias = query.alias.as_deref().unwrap_or(table_name);
1525    crate::storage::query::sql_lowering::effective_table_filter(query).is_none_or(|filter| {
1526        super::join_filter::evaluate_runtime_filter_with_db(
1527            Some(&runtime.inner.db),
1528            record,
1529            &filter,
1530            Some(table_name),
1531            Some(table_alias),
1532        )
1533    })
1534}
1535
1536fn projection_unqualified_column(projection: &Projection) -> Option<&str> {
1537    match projection {
1538        Projection::Field(FieldRef::TableColumn { table, column }, _) if table.is_empty() => {
1539            Some(column.as_str())
1540        }
1541        Projection::Column(column) => Some(column.as_str()),
1542        Projection::Alias(column, _) => Some(column.as_str()),
1543        _ => None,
1544    }
1545}
1546
1547fn projection_function(projection: &Projection) -> Option<(&str, &[Projection])> {
1548    match projection {
1549        Projection::Function(name, args) => {
1550            let function = name.split_once(':').map(|(name, _)| name).unwrap_or(name);
1551            Some((function, args.as_slice()))
1552        }
1553        _ => None,
1554    }
1555}
1556
1557fn projection_single_text_arg(function: &str, args: &[Projection]) -> RedDBResult<String> {
1558    if args.len() != 1 {
1559        return Err(RedDBError::Query(format!(
1560            "{function}(...) expects exactly one string literal"
1561        )));
1562    }
1563    match &args[0] {
1564        Projection::Column(column) => column
1565            .strip_prefix("LIT:")
1566            .map(ToString::to_string)
1567            .ok_or_else(|| {
1568                RedDBError::Query(format!("{function}(...) expects a string literal argument"))
1569            }),
1570        _ => Err(RedDBError::Query(format!(
1571            "{function}(...) expects a string literal argument"
1572        ))),
1573    }
1574}
1575
1576fn probabilistic_projection_label(projection: &Projection, base: &str, index: usize) -> String {
1577    match projection {
1578        Projection::Field(FieldRef::TableColumn { column, .. }, Some(alias))
1579            if alias.eq_ignore_ascii_case(column) =>
1580        {
1581            numbered_probabilistic_label(base, index)
1582        }
1583        Projection::Field(_, Some(alias)) => alias.clone(),
1584        Projection::Alias(column, alias) if column.eq_ignore_ascii_case(alias) => {
1585            numbered_probabilistic_label(base, index)
1586        }
1587        Projection::Alias(_, alias) => alias.clone(),
1588        Projection::Function(name, _) => name
1589            .split_once(':')
1590            .map(|(_, alias)| {
1591                if is_generated_probabilistic_function_label(alias, base) {
1592                    numbered_probabilistic_label(base, index)
1593                } else {
1594                    alias.to_string()
1595                }
1596            })
1597            .unwrap_or_else(|| numbered_probabilistic_label(base, index)),
1598        _ => numbered_probabilistic_label(base, index),
1599    }
1600}
1601
1602fn is_generated_probabilistic_function_label(alias: &str, base: &str) -> bool {
1603    alias
1604        .get(..base.len())
1605        .is_some_and(|head| head.eq_ignore_ascii_case(base))
1606        && alias[base.len()..].starts_with('(')
1607}
1608
1609fn numbered_probabilistic_label(base: &str, index: usize) -> String {
1610    if index == 0 {
1611        base.to_string()
1612    } else {
1613        format!("{base}_{}", index + 1)
1614    }
1615}
1616
1617fn probabilistic_projection_form(projection: &ProbabilisticReadProjection) -> &'static str {
1618    match projection {
1619        ProbabilisticReadProjection::Cardinality { .. } => "SELECT CARDINALITY",
1620        ProbabilisticReadProjection::Freq { .. } => "FREQ(...)",
1621        ProbabilisticReadProjection::Contains { .. } => "CONTAINS(...)",
1622    }
1623}