1use 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 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 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 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 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 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}