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 = 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 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 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 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 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 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}