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 = reddb_rql::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        if let ProbabilisticCommand::CreateHll { name, .. }
727        | ProbabilisticCommand::CreateSketch { name, .. }
728        | ProbabilisticCommand::CreateFilter { name, .. } = cmd
729        {
730            if super::impl_ddl::is_system_schema_name(name) {
731                return Err(RedDBError::Query("system schema is read-only".to_string()));
732            }
733        }
734        match cmd {
735            // ── HyperLogLog ──────────────────────────────────────────
736            ProbabilisticCommand::CreateHll {
737                name,
738                precision,
739                if_not_exists,
740            } => {
741                let mut hlls =
742                    probabilistic_write(&self.inner.probabilistic.hlls, "probabilistic HLL store");
743                if hlls.contains_key(name) {
744                    if *if_not_exists {
745                        return Ok(RuntimeQueryResult::ok_message(
746                            raw_query.to_string(),
747                            &format!("HLL '{}' already exists", name),
748                            "create",
749                        ));
750                    }
751                    return Err(RedDBError::Query(format!("HLL '{}' already exists", name)));
752                }
753                let hll = crate::storage::primitives::hyperloglog::HyperLogLog::with_precision(
754                    *precision,
755                )
756                .ok_or_else(|| {
757                    RedDBError::Query(format!(
758                        "HLL precision must be between 4 and 18, got {precision}"
759                    ))
760                })?;
761                self.create_probabilistic_catalog_entry(
762                    name,
763                    crate::catalog::CollectionModel::Hll,
764                )?;
765                self.persist_probabilistic_blob(PROB_HLL_STATE_PREFIX, name, hll.as_bytes())?;
766                self.append_probabilistic_snapshot_delta(
767                    PROB_WAL_KIND_HLL,
768                    name,
769                    hll.as_bytes().to_vec(),
770                )?;
771                hlls.insert(name.clone(), hll);
772                Ok(RuntimeQueryResult::ok_message(
773                    raw_query.to_string(),
774                    &format!("HLL '{}' created", name),
775                    "create",
776                ))
777            }
778            ProbabilisticCommand::HllAdd { name, elements } => {
779                let mut hlls =
780                    probabilistic_write(&self.inner.probabilistic.hlls, "probabilistic HLL store");
781                let hll = hlls
782                    .get_mut(name)
783                    .ok_or_else(|| RedDBError::NotFound(format!("HLL '{}' not found", name)))?;
784                for elem in elements {
785                    hll.add(elem.as_bytes());
786                }
787                self.append_probabilistic_add_delta(
788                    PROB_WAL_KIND_HLL,
789                    name,
790                    elements
791                        .iter()
792                        .map(|element| element.as_bytes().to_vec())
793                        .collect(),
794                )?;
795                Ok(RuntimeQueryResult::ok_message(
796                    raw_query.to_string(),
797                    &format!("{} element(s) added to HLL '{}'", elements.len(), name),
798                    "insert",
799                ))
800            }
801            ProbabilisticCommand::HllCount { names } => {
802                let hlls =
803                    probabilistic_read(&self.inner.probabilistic.hlls, "probabilistic HLL store");
804                if names.is_empty() {
805                    return Err(RedDBError::Query(
806                        "HLL COUNT requires at least one HLL name".to_string(),
807                    ));
808                }
809                if names.len() == 1 {
810                    let hll = hlls.get(&names[0]).ok_or_else(|| {
811                        RedDBError::NotFound(format!("HLL '{}' not found", names[0]))
812                    })?;
813                    let count = hll.count();
814                    let mut result = UnifiedResult::with_columns(vec!["count".into()]);
815                    let mut record = UnifiedRecord::new();
816                    record.set("count", Value::UnsignedInteger(count));
817                    result.push(record);
818                    Ok(RuntimeQueryResult {
819                        query: raw_query.to_string(),
820                        mode: QueryMode::Sql,
821                        statement: "hll_count",
822                        engine: "runtime-probabilistic",
823                        result,
824                        affected_rows: 0,
825                        statement_type: "select",
826                        bookmark: None,
827                        notice: None,
828                    })
829                } else {
830                    // Multi-HLL count = union count
831                    let first_name = &names[0];
832                    let first = hlls.get(first_name).ok_or_else(|| {
833                        RedDBError::NotFound(format!("HLL '{}' not found", first_name))
834                    })?;
835                    let expected_precision = first.precision();
836                    let mut merged =
837                        crate::storage::primitives::hyperloglog::HyperLogLog::with_precision(
838                            expected_precision,
839                        )
840                        .expect("loaded HLL precision is valid");
841                    for name in names {
842                        let hll = hlls.get(name).ok_or_else(|| {
843                            RedDBError::NotFound(format!("HLL '{}' not found", name))
844                        })?;
845                        if hll.precision() != expected_precision {
846                            return Err(hll_precision_mismatch_error(
847                                "COUNT",
848                                first_name,
849                                expected_precision,
850                                name,
851                                hll.precision(),
852                            ));
853                        }
854                        merged.merge(hll);
855                    }
856                    let count = merged.count();
857                    let mut result = UnifiedResult::with_columns(vec!["count".into()]);
858                    let mut record = UnifiedRecord::new();
859                    record.set("count", Value::UnsignedInteger(count));
860                    result.push(record);
861                    Ok(RuntimeQueryResult {
862                        query: raw_query.to_string(),
863                        mode: QueryMode::Sql,
864                        statement: "hll_count",
865                        engine: "runtime-probabilistic",
866                        result,
867                        affected_rows: 0,
868                        statement_type: "select",
869                        bookmark: None,
870                        notice: None,
871                    })
872                }
873            }
874            ProbabilisticCommand::HllMerge { dest, sources } => {
875                let mut hlls =
876                    probabilistic_write(&self.inner.probabilistic.hlls, "probabilistic HLL store");
877                let first_src = sources.first().ok_or_else(|| {
878                    RedDBError::Query("HLL MERGE requires at least one source HLL".to_string())
879                })?;
880                let first = hlls.get(first_src).ok_or_else(|| {
881                    RedDBError::NotFound(format!("HLL '{}' not found", first_src))
882                })?;
883                let expected_precision = first.precision();
884                let mut merged =
885                    crate::storage::primitives::hyperloglog::HyperLogLog::with_precision(
886                        expected_precision,
887                    )
888                    .expect("loaded HLL precision is valid");
889                for src in sources {
890                    let hll = hlls
891                        .get(src)
892                        .ok_or_else(|| RedDBError::NotFound(format!("HLL '{}' not found", src)))?;
893                    if hll.precision() != expected_precision {
894                        return Err(hll_precision_mismatch_error(
895                            "MERGE",
896                            first_src,
897                            expected_precision,
898                            src,
899                            hll.precision(),
900                        ));
901                    }
902                    merged.merge(hll);
903                }
904                self.persist_probabilistic_blob(PROB_HLL_STATE_PREFIX, dest, merged.as_bytes())?;
905                self.append_probabilistic_snapshot_delta(
906                    PROB_WAL_KIND_HLL,
907                    dest,
908                    merged.as_bytes().to_vec(),
909                )?;
910                hlls.insert(dest.clone(), merged);
911                Ok(RuntimeQueryResult::ok_message(
912                    raw_query.to_string(),
913                    &format!(
914                        "HLL '{}' created from merge of {}",
915                        dest,
916                        sources.join(", ")
917                    ),
918                    "create",
919                ))
920            }
921            ProbabilisticCommand::HllInfo { name } => {
922                let hlls =
923                    probabilistic_read(&self.inner.probabilistic.hlls, "probabilistic HLL store");
924                let hll = hlls
925                    .get(name)
926                    .ok_or_else(|| RedDBError::NotFound(format!("HLL '{}' not found", name)))?;
927                let mut result = UnifiedResult::with_columns(vec![
928                    "name".into(),
929                    "precision".into(),
930                    "count".into(),
931                    "memory_bytes".into(),
932                ]);
933                let mut record = UnifiedRecord::new();
934                record.set("name", Value::text(name.clone()));
935                record.set("precision", Value::UnsignedInteger(hll.precision() as u64));
936                record.set("count", Value::UnsignedInteger(hll.count()));
937                record.set(
938                    "memory_bytes",
939                    Value::UnsignedInteger(hll.memory_bytes() as u64),
940                );
941                result.push(record);
942                Ok(RuntimeQueryResult {
943                    query: raw_query.to_string(),
944                    mode: QueryMode::Sql,
945                    statement: "hll_info",
946                    engine: "runtime-probabilistic",
947                    result,
948                    affected_rows: 0,
949                    statement_type: "select",
950                    bookmark: None,
951                    notice: None,
952                })
953            }
954            ProbabilisticCommand::DropHll { name, if_exists } => {
955                let mut hlls =
956                    probabilistic_write(&self.inner.probabilistic.hlls, "probabilistic HLL store");
957                if hlls.remove(name).is_none() {
958                    if *if_exists {
959                        return Ok(RuntimeQueryResult::ok_message(
960                            raw_query.to_string(),
961                            &format!("HLL '{}' does not exist", name),
962                            "drop",
963                        ));
964                    }
965                    return Err(RedDBError::NotFound(format!("HLL '{}' not found", name)));
966                }
967                self.drop_probabilistic_catalog_entry(name)?;
968                self.delete_probabilistic_blob(PROB_HLL_STATE_PREFIX, name)?;
969                self.append_probabilistic_drop_delta(PROB_WAL_KIND_HLL, name)?;
970                Ok(RuntimeQueryResult::ok_message(
971                    raw_query.to_string(),
972                    &format!("HLL '{}' dropped", name),
973                    "drop",
974                ))
975            }
976
977            // ── Count-Min Sketch ───────────────────────────────────────
978            ProbabilisticCommand::CreateSketch {
979                name,
980                width,
981                depth,
982                if_not_exists,
983            } => {
984                let mut sketches = probabilistic_write(
985                    &self.inner.probabilistic.sketches,
986                    "probabilistic sketch store",
987                );
988                if sketches.contains_key(name) {
989                    if *if_not_exists {
990                        return Ok(RuntimeQueryResult::ok_message(
991                            raw_query.to_string(),
992                            &format!("SKETCH '{}' already exists", name),
993                            "create",
994                        ));
995                    }
996                    return Err(RedDBError::Query(format!(
997                        "SKETCH '{}' already exists",
998                        name
999                    )));
1000                }
1001                self.create_probabilistic_catalog_entry(
1002                    name,
1003                    crate::catalog::CollectionModel::Sketch,
1004                )?;
1005                let sketch = crate::storage::primitives::count_min_sketch::CountMinSketch::new(
1006                    *width, *depth,
1007                );
1008                self.persist_probabilistic_blob(
1009                    PROB_SKETCH_STATE_PREFIX,
1010                    name,
1011                    &sketch.as_bytes(),
1012                )?;
1013                self.append_probabilistic_snapshot_delta(
1014                    PROB_WAL_KIND_SKETCH,
1015                    name,
1016                    sketch.as_bytes(),
1017                )?;
1018                sketches.insert(name.clone(), sketch);
1019                Ok(RuntimeQueryResult::ok_message(
1020                    raw_query.to_string(),
1021                    &format!(
1022                        "SKETCH '{}' created (width={}, depth={})",
1023                        name, width, depth
1024                    ),
1025                    "create",
1026                ))
1027            }
1028            ProbabilisticCommand::SketchAdd {
1029                name,
1030                element,
1031                count,
1032            } => {
1033                let mut sketches = probabilistic_write(
1034                    &self.inner.probabilistic.sketches,
1035                    "probabilistic sketch store",
1036                );
1037                let sketch = sketches
1038                    .get_mut(name)
1039                    .ok_or_else(|| RedDBError::NotFound(format!("SKETCH '{}' not found", name)))?;
1040                sketch.add(element.as_bytes(), *count);
1041                self.append_probabilistic_add_delta(
1042                    PROB_WAL_KIND_SKETCH,
1043                    name,
1044                    vec![element.as_bytes().to_vec(), count.to_le_bytes().to_vec()],
1045                )?;
1046                Ok(RuntimeQueryResult::ok_message(
1047                    raw_query.to_string(),
1048                    &format!("added {} to SKETCH '{}'", count, name),
1049                    "insert",
1050                ))
1051            }
1052            ProbabilisticCommand::SketchCount { name, element } => {
1053                let sketches = probabilistic_read(
1054                    &self.inner.probabilistic.sketches,
1055                    "probabilistic sketch store",
1056                );
1057                let sketch = sketches
1058                    .get(name)
1059                    .ok_or_else(|| RedDBError::NotFound(format!("SKETCH '{}' not found", name)))?;
1060                let estimate = sketch.estimate(element.as_bytes());
1061                let mut result = UnifiedResult::with_columns(vec!["estimate".into()]);
1062                let mut record = UnifiedRecord::new();
1063                record.set("estimate", Value::UnsignedInteger(estimate));
1064                result.push(record);
1065                Ok(RuntimeQueryResult {
1066                    query: raw_query.to_string(),
1067                    mode: QueryMode::Sql,
1068                    statement: "sketch_count",
1069                    engine: "runtime-probabilistic",
1070                    result,
1071                    affected_rows: 0,
1072                    statement_type: "select",
1073                    bookmark: None,
1074                    notice: None,
1075                })
1076            }
1077            ProbabilisticCommand::SketchMerge { dest, sources } => {
1078                let mut sketches = probabilistic_write(
1079                    &self.inner.probabilistic.sketches,
1080                    "probabilistic sketch store",
1081                );
1082                let first_src = sketches.get(&sources[0]).ok_or_else(|| {
1083                    RedDBError::NotFound(format!("SKETCH '{}' not found", sources[0]))
1084                })?;
1085                let mut merged = crate::storage::primitives::count_min_sketch::CountMinSketch::new(
1086                    first_src.width(),
1087                    first_src.depth(),
1088                );
1089                for src in sources {
1090                    let sketch = sketches.get(src).ok_or_else(|| {
1091                        RedDBError::NotFound(format!("SKETCH '{}' not found", src))
1092                    })?;
1093                    if !merged.merge(sketch) {
1094                        return Err(RedDBError::Query(format!(
1095                            "SKETCH '{}' has incompatible dimensions",
1096                            src
1097                        )));
1098                    }
1099                }
1100                self.persist_probabilistic_blob(
1101                    PROB_SKETCH_STATE_PREFIX,
1102                    dest,
1103                    &merged.as_bytes(),
1104                )?;
1105                self.append_probabilistic_snapshot_delta(
1106                    PROB_WAL_KIND_SKETCH,
1107                    dest,
1108                    merged.as_bytes(),
1109                )?;
1110                sketches.insert(dest.clone(), merged);
1111                Ok(RuntimeQueryResult::ok_message(
1112                    raw_query.to_string(),
1113                    &format!(
1114                        "SKETCH '{}' created from merge of {}",
1115                        dest,
1116                        sources.join(", ")
1117                    ),
1118                    "create",
1119                ))
1120            }
1121            ProbabilisticCommand::SketchInfo { name } => {
1122                let sketches = probabilistic_read(
1123                    &self.inner.probabilistic.sketches,
1124                    "probabilistic sketch store",
1125                );
1126                let sketch = sketches
1127                    .get(name)
1128                    .ok_or_else(|| RedDBError::NotFound(format!("SKETCH '{}' not found", name)))?;
1129                let mut result = UnifiedResult::with_columns(vec![
1130                    "name".into(),
1131                    "width".into(),
1132                    "depth".into(),
1133                    "total".into(),
1134                    "memory_bytes".into(),
1135                ]);
1136                let mut record = UnifiedRecord::new();
1137                record.set("name", Value::text(name.clone()));
1138                record.set("width", Value::UnsignedInteger(sketch.width() as u64));
1139                record.set("depth", Value::UnsignedInteger(sketch.depth() as u64));
1140                record.set("total", Value::UnsignedInteger(sketch.total()));
1141                record.set(
1142                    "memory_bytes",
1143                    Value::UnsignedInteger(sketch.memory_bytes() as u64),
1144                );
1145                result.push(record);
1146                Ok(RuntimeQueryResult {
1147                    query: raw_query.to_string(),
1148                    mode: QueryMode::Sql,
1149                    statement: "sketch_info",
1150                    engine: "runtime-probabilistic",
1151                    result,
1152                    affected_rows: 0,
1153                    statement_type: "select",
1154                    bookmark: None,
1155                    notice: None,
1156                })
1157            }
1158            ProbabilisticCommand::DropSketch { name, if_exists } => {
1159                let mut sketches = probabilistic_write(
1160                    &self.inner.probabilistic.sketches,
1161                    "probabilistic sketch store",
1162                );
1163                if sketches.remove(name).is_none() {
1164                    if *if_exists {
1165                        return Ok(RuntimeQueryResult::ok_message(
1166                            raw_query.to_string(),
1167                            &format!("SKETCH '{}' does not exist", name),
1168                            "drop",
1169                        ));
1170                    }
1171                    return Err(RedDBError::NotFound(format!("SKETCH '{}' not found", name)));
1172                }
1173                self.drop_probabilistic_catalog_entry(name)?;
1174                self.delete_probabilistic_blob(PROB_SKETCH_STATE_PREFIX, name)?;
1175                self.append_probabilistic_drop_delta(PROB_WAL_KIND_SKETCH, name)?;
1176                Ok(RuntimeQueryResult::ok_message(
1177                    raw_query.to_string(),
1178                    &format!("SKETCH '{}' dropped", name),
1179                    "drop",
1180                ))
1181            }
1182
1183            // ── Cuckoo Filter ─────────────────────────────────────────
1184            ProbabilisticCommand::CreateFilter {
1185                name,
1186                capacity,
1187                if_not_exists,
1188            } => {
1189                let mut filters = probabilistic_write(
1190                    &self.inner.probabilistic.filters,
1191                    "probabilistic filter store",
1192                );
1193                if filters.contains_key(name) {
1194                    if *if_not_exists {
1195                        return Ok(RuntimeQueryResult::ok_message(
1196                            raw_query.to_string(),
1197                            &format!("FILTER '{}' already exists", name),
1198                            "create",
1199                        ));
1200                    }
1201                    return Err(RedDBError::Query(format!(
1202                        "FILTER '{}' already exists",
1203                        name
1204                    )));
1205                }
1206                self.create_probabilistic_catalog_entry(
1207                    name,
1208                    crate::catalog::CollectionModel::Filter,
1209                )?;
1210                let filter =
1211                    crate::storage::primitives::cuckoo_filter::CuckooFilter::new(*capacity);
1212                self.persist_probabilistic_blob(
1213                    PROB_FILTER_STATE_PREFIX,
1214                    name,
1215                    &filter.as_bytes(),
1216                )?;
1217                self.append_probabilistic_snapshot_delta(
1218                    PROB_WAL_KIND_FILTER,
1219                    name,
1220                    filter.as_bytes(),
1221                )?;
1222                filters.insert(name.clone(), filter);
1223                Ok(RuntimeQueryResult::ok_message(
1224                    raw_query.to_string(),
1225                    &format!("FILTER '{}' created (capacity={})", name, capacity),
1226                    "create",
1227                ))
1228            }
1229            ProbabilisticCommand::FilterAdd { name, element } => {
1230                let mut filters = probabilistic_write(
1231                    &self.inner.probabilistic.filters,
1232                    "probabilistic filter store",
1233                );
1234                let filter = filters
1235                    .get_mut(name)
1236                    .ok_or_else(|| RedDBError::NotFound(format!("FILTER '{}' not found", name)))?;
1237                if !filter.insert(element.as_bytes()) {
1238                    return Err(RedDBError::Query(format!("FILTER '{}' is full", name)));
1239                }
1240                self.append_probabilistic_add_delta(
1241                    PROB_WAL_KIND_FILTER,
1242                    name,
1243                    vec![element.as_bytes().to_vec()],
1244                )?;
1245                Ok(RuntimeQueryResult::ok_message(
1246                    raw_query.to_string(),
1247                    &format!("element added to FILTER '{}'", name),
1248                    "insert",
1249                ))
1250            }
1251            ProbabilisticCommand::FilterCheck { name, element } => {
1252                let filters = probabilistic_read(
1253                    &self.inner.probabilistic.filters,
1254                    "probabilistic filter store",
1255                );
1256                let filter = filters
1257                    .get(name)
1258                    .ok_or_else(|| RedDBError::NotFound(format!("FILTER '{}' not found", name)))?;
1259                let exists = filter.contains(element.as_bytes());
1260                let mut result = UnifiedResult::with_columns(vec!["exists".into()]);
1261                let mut record = UnifiedRecord::new();
1262                record.set("exists", Value::Boolean(exists));
1263                result.push(record);
1264                Ok(RuntimeQueryResult {
1265                    query: raw_query.to_string(),
1266                    mode: QueryMode::Sql,
1267                    statement: "filter_check",
1268                    engine: "runtime-probabilistic",
1269                    result,
1270                    affected_rows: 0,
1271                    statement_type: "select",
1272                    bookmark: None,
1273                    notice: None,
1274                })
1275            }
1276            ProbabilisticCommand::FilterDelete { name, element } => {
1277                let mut filters = probabilistic_write(
1278                    &self.inner.probabilistic.filters,
1279                    "probabilistic filter store",
1280                );
1281                let filter = filters
1282                    .get_mut(name)
1283                    .ok_or_else(|| RedDBError::NotFound(format!("FILTER '{}' not found", name)))?;
1284                let removed = filter.delete(element.as_bytes());
1285                self.append_probabilistic_delete_delta(
1286                    PROB_WAL_KIND_FILTER,
1287                    name,
1288                    vec![element.as_bytes().to_vec()],
1289                )?;
1290                Ok(RuntimeQueryResult::ok_message(
1291                    raw_query.to_string(),
1292                    &format!(
1293                        "element {} from FILTER '{}'",
1294                        if removed { "deleted" } else { "not found in" },
1295                        name
1296                    ),
1297                    "delete",
1298                ))
1299            }
1300            ProbabilisticCommand::FilterCount { name } => {
1301                let filters = probabilistic_read(
1302                    &self.inner.probabilistic.filters,
1303                    "probabilistic filter store",
1304                );
1305                let filter = filters
1306                    .get(name)
1307                    .ok_or_else(|| RedDBError::NotFound(format!("FILTER '{}' not found", name)))?;
1308                let mut result = UnifiedResult::with_columns(vec!["count".into()]);
1309                let mut record = UnifiedRecord::new();
1310                record.set("count", Value::UnsignedInteger(filter.count() as u64));
1311                result.push(record);
1312                Ok(RuntimeQueryResult {
1313                    query: raw_query.to_string(),
1314                    mode: QueryMode::Sql,
1315                    statement: "filter_count",
1316                    engine: "runtime-probabilistic",
1317                    result,
1318                    affected_rows: 0,
1319                    statement_type: "select",
1320                    bookmark: None,
1321                    notice: None,
1322                })
1323            }
1324            ProbabilisticCommand::FilterInfo { name } => {
1325                let filters = probabilistic_read(
1326                    &self.inner.probabilistic.filters,
1327                    "probabilistic filter store",
1328                );
1329                let filter = filters
1330                    .get(name)
1331                    .ok_or_else(|| RedDBError::NotFound(format!("FILTER '{}' not found", name)))?;
1332                let mut result = UnifiedResult::with_columns(vec![
1333                    "name".into(),
1334                    "count".into(),
1335                    "load_factor".into(),
1336                    "memory_bytes".into(),
1337                ]);
1338                let mut record = UnifiedRecord::new();
1339                record.set("name", Value::text(name.clone()));
1340                record.set("count", Value::UnsignedInteger(filter.count() as u64));
1341                record.set("load_factor", Value::Float(filter.load_factor()));
1342                record.set(
1343                    "memory_bytes",
1344                    Value::UnsignedInteger(filter.memory_bytes() as u64),
1345                );
1346                result.push(record);
1347                Ok(RuntimeQueryResult {
1348                    query: raw_query.to_string(),
1349                    mode: QueryMode::Sql,
1350                    statement: "filter_info",
1351                    engine: "runtime-probabilistic",
1352                    result,
1353                    affected_rows: 0,
1354                    statement_type: "select",
1355                    bookmark: None,
1356                    notice: None,
1357                })
1358            }
1359            ProbabilisticCommand::DropFilter { name, if_exists } => {
1360                let mut filters = probabilistic_write(
1361                    &self.inner.probabilistic.filters,
1362                    "probabilistic filter store",
1363                );
1364                if filters.remove(name).is_none() {
1365                    if *if_exists {
1366                        return Ok(RuntimeQueryResult::ok_message(
1367                            raw_query.to_string(),
1368                            &format!("FILTER '{}' does not exist", name),
1369                            "drop",
1370                        ));
1371                    }
1372                    return Err(RedDBError::NotFound(format!("FILTER '{}' not found", name)));
1373                }
1374                self.drop_probabilistic_catalog_entry(name)?;
1375                self.delete_probabilistic_blob(PROB_FILTER_STATE_PREFIX, name)?;
1376                self.append_probabilistic_drop_delta(PROB_WAL_KIND_FILTER, name)?;
1377                Ok(RuntimeQueryResult::ok_message(
1378                    raw_query.to_string(),
1379                    &format!("FILTER '{}' dropped", name),
1380                    "drop",
1381                ))
1382            }
1383        }
1384    }
1385}
1386
1387fn parse_probabilistic_read_projection(
1388    projection: &Projection,
1389    index: usize,
1390) -> RedDBResult<Option<ProbabilisticReadProjection>> {
1391    if let Some(column) = projection_unqualified_column(projection) {
1392        if column.eq_ignore_ascii_case("CARDINALITY") {
1393            return Ok(Some(ProbabilisticReadProjection::Cardinality {
1394                label: probabilistic_projection_label(projection, "cardinality", index),
1395            }));
1396        }
1397    }
1398
1399    let Some((function, args)) = projection_function(projection) else {
1400        return Ok(None);
1401    };
1402    if function.eq_ignore_ascii_case("FREQ") {
1403        let element = projection_single_text_arg(function, args)?;
1404        return Ok(Some(ProbabilisticReadProjection::Freq {
1405            element,
1406            label: probabilistic_projection_label(projection, "freq", index),
1407        }));
1408    }
1409    if function.eq_ignore_ascii_case("CONTAINS") {
1410        let element = projection_single_text_arg(function, args)?;
1411        return Ok(Some(ProbabilisticReadProjection::Contains {
1412            element,
1413            label: probabilistic_projection_label(projection, "contains", index),
1414        }));
1415    }
1416
1417    Ok(None)
1418}
1419
1420fn single_probabilistic_operand(mut operands: Vec<Vec<u8>>, context: &str) -> RedDBResult<Vec<u8>> {
1421    if operands.len() != 1 {
1422        return Err(RedDBError::Internal(format!(
1423            "{context} WAL delta expected one operand, got {}",
1424            operands.len()
1425        )));
1426    }
1427    Ok(operands.remove(0))
1428}
1429
1430fn sketch_add_operands(mut operands: Vec<Vec<u8>>) -> RedDBResult<(Vec<u8>, u64)> {
1431    if operands.len() != 2 {
1432        return Err(RedDBError::Internal(format!(
1433            "SKETCH ADD WAL delta expected two operands, got {}",
1434            operands.len()
1435        )));
1436    }
1437    let count_bytes = operands.remove(1);
1438    let count_array: [u8; 8] = count_bytes.as_slice().try_into().map_err(|_| {
1439        RedDBError::Internal(format!(
1440            "SKETCH ADD WAL delta count expected 8 bytes, got {}",
1441            count_bytes.len()
1442        ))
1443    })?;
1444    Ok((operands.remove(0), u64::from_le_bytes(count_array)))
1445}
1446
1447fn validate_probabilistic_read_model(
1448    collection: &str,
1449    actual_model: crate::catalog::CollectionModel,
1450    projections: &[ProbabilisticReadProjection],
1451) -> RedDBResult<()> {
1452    for projection in projections {
1453        let expected_model = match projection {
1454            ProbabilisticReadProjection::Cardinality { .. } => crate::catalog::CollectionModel::Hll,
1455            ProbabilisticReadProjection::Freq { .. } => crate::catalog::CollectionModel::Sketch,
1456            ProbabilisticReadProjection::Contains { .. } => crate::catalog::CollectionModel::Filter,
1457        };
1458        if actual_model != expected_model {
1459            return Err(RedDBError::Query(format!(
1460                "{} is only supported for {} collections; '{}' is {}",
1461                probabilistic_projection_form(projection),
1462                crate::runtime::ddl::polymorphic_resolver::model_name(expected_model),
1463                collection,
1464                crate::runtime::ddl::polymorphic_resolver::model_name(actual_model)
1465            )));
1466        }
1467    }
1468    Ok(())
1469}
1470
1471impl RedDBRuntime {
1472    fn materialize_probabilistic_select_row(
1473        &self,
1474        collection: &str,
1475        projections: &[ProbabilisticReadProjection],
1476    ) -> RedDBResult<(Vec<String>, UnifiedRecord)> {
1477        let mut columns = Vec::with_capacity(projections.len());
1478        let mut record = UnifiedRecord::new();
1479        for projection in projections {
1480            match projection {
1481                ProbabilisticReadProjection::Cardinality { label } => {
1482                    let hlls = probabilistic_read(
1483                        &self.inner.probabilistic.hlls,
1484                        "probabilistic HLL store",
1485                    );
1486                    let hll = hlls.get(collection).ok_or_else(|| {
1487                        RedDBError::NotFound(format!("HLL '{}' not found", collection))
1488                    })?;
1489                    columns.push(label.clone());
1490                    record.set(label, Value::UnsignedInteger(hll.count()));
1491                }
1492                ProbabilisticReadProjection::Freq { element, label } => {
1493                    let sketches = probabilistic_read(
1494                        &self.inner.probabilistic.sketches,
1495                        "probabilistic sketch store",
1496                    );
1497                    let sketch = sketches.get(collection).ok_or_else(|| {
1498                        RedDBError::NotFound(format!("SKETCH '{}' not found", collection))
1499                    })?;
1500                    columns.push(label.clone());
1501                    record.set(
1502                        label,
1503                        Value::UnsignedInteger(sketch.estimate(element.as_bytes())),
1504                    );
1505                }
1506                ProbabilisticReadProjection::Contains { element, label } => {
1507                    let filters = probabilistic_read(
1508                        &self.inner.probabilistic.filters,
1509                        "probabilistic filter store",
1510                    );
1511                    let filter = filters.get(collection).ok_or_else(|| {
1512                        RedDBError::NotFound(format!("FILTER '{}' not found", collection))
1513                    })?;
1514                    columns.push(label.clone());
1515                    record.set(label, Value::Boolean(filter.contains(element.as_bytes())));
1516                }
1517            }
1518        }
1519        Ok((columns, record))
1520    }
1521}
1522
1523fn probabilistic_select_row_visible(
1524    runtime: &RedDBRuntime,
1525    query: &TableQuery,
1526    record: &UnifiedRecord,
1527) -> bool {
1528    if query.limit == Some(0) || query.offset.is_some_and(|offset| offset > 0) {
1529        return false;
1530    }
1531    let table_name = query.table.as_str();
1532    let table_alias = query.alias.as_deref().unwrap_or(table_name);
1533    reddb_rql::sql_lowering::effective_table_filter(query).is_none_or(|filter| {
1534        super::join_filter::evaluate_runtime_filter_with_db(
1535            Some(&runtime.inner.db),
1536            record,
1537            &filter,
1538            Some(table_name),
1539            Some(table_alias),
1540        )
1541    })
1542}
1543
1544fn projection_unqualified_column(projection: &Projection) -> Option<&str> {
1545    match projection {
1546        Projection::Field(FieldRef::TableColumn { table, column }, _) if table.is_empty() => {
1547            Some(column.as_str())
1548        }
1549        Projection::Column(column) => Some(column.as_str()),
1550        Projection::Alias(column, _) => Some(column.as_str()),
1551        _ => None,
1552    }
1553}
1554
1555fn projection_function(projection: &Projection) -> Option<(&str, &[Projection])> {
1556    match projection {
1557        Projection::Function(name, args) => {
1558            let function = name.split_once(':').map(|(name, _)| name).unwrap_or(name);
1559            Some((function, args.as_slice()))
1560        }
1561        _ => None,
1562    }
1563}
1564
1565fn projection_single_text_arg(function: &str, args: &[Projection]) -> RedDBResult<String> {
1566    if args.len() != 1 {
1567        return Err(RedDBError::Query(format!(
1568            "{function}(...) expects exactly one string literal"
1569        )));
1570    }
1571    match &args[0] {
1572        Projection::Column(column) => column
1573            .strip_prefix("LIT:")
1574            .map(ToString::to_string)
1575            .ok_or_else(|| {
1576                RedDBError::Query(format!("{function}(...) expects a string literal argument"))
1577            }),
1578        _ => Err(RedDBError::Query(format!(
1579            "{function}(...) expects a string literal argument"
1580        ))),
1581    }
1582}
1583
1584fn probabilistic_projection_label(projection: &Projection, base: &str, index: usize) -> String {
1585    match projection {
1586        Projection::Field(FieldRef::TableColumn { column, .. }, Some(alias))
1587            if alias.eq_ignore_ascii_case(column) =>
1588        {
1589            numbered_probabilistic_label(base, index)
1590        }
1591        Projection::Field(_, Some(alias)) => alias.clone(),
1592        Projection::Alias(column, alias) if column.eq_ignore_ascii_case(alias) => {
1593            numbered_probabilistic_label(base, index)
1594        }
1595        Projection::Alias(_, alias) => alias.clone(),
1596        Projection::Function(name, _) => name
1597            .split_once(':')
1598            .map(|(_, alias)| {
1599                if is_generated_probabilistic_function_label(alias, base) {
1600                    numbered_probabilistic_label(base, index)
1601                } else {
1602                    alias.to_string()
1603                }
1604            })
1605            .unwrap_or_else(|| numbered_probabilistic_label(base, index)),
1606        _ => numbered_probabilistic_label(base, index),
1607    }
1608}
1609
1610fn is_generated_probabilistic_function_label(alias: &str, base: &str) -> bool {
1611    alias
1612        .get(..base.len())
1613        .is_some_and(|head| head.eq_ignore_ascii_case(base))
1614        && alias[base.len()..].starts_with('(')
1615}
1616
1617fn numbered_probabilistic_label(base: &str, index: usize) -> String {
1618    if index == 0 {
1619        base.to_string()
1620    } else {
1621        format!("{base}_{}", index + 1)
1622    }
1623}
1624
1625fn probabilistic_projection_form(projection: &ProbabilisticReadProjection) -> &'static str {
1626    match projection {
1627        ProbabilisticReadProjection::Cardinality { .. } => "SELECT CARDINALITY",
1628        ProbabilisticReadProjection::Freq { .. } => "FREQ(...)",
1629        ProbabilisticReadProjection::Contains { .. } => "CONTAINS(...)",
1630    }
1631}