1use std::collections::{HashMap, HashSet};
7use std::sync::{Arc, OnceLock};
8
9#[cfg(test)]
10use xlog_core::ScalarType;
11use xlog_core::{RelId, Result, RuntimeConfig, Schema, XlogError};
12use xlog_cuda::memory::TrackedCudaSlice;
13use xlog_cuda::{CudaBuffer, CudaKernelProvider};
14#[cfg(test)]
15use xlog_ir::{CompareOp, ConstValue, Stratum};
16use xlog_ir::{ExecutionPlan, Expr, JoinType, ProjectExpr, RirNode};
17use xlog_stats::{StatsManager, StatsSnapshot};
18
19use crate::ilp_registry::{IlpRegistry, IlpTaggedResult};
20use crate::profiler::{ExecutionStats, Profiler};
21use crate::RelationStore;
22
23mod delta;
24mod epistemic_workspace;
25mod expression;
26mod join_cache;
27mod node_dispatch;
28mod recursive;
29mod rewrite;
30mod wcoj_cost_model;
31mod wcoj_dispatch;
32#[cfg(feature = "wcoj-phase-timing")]
33pub mod wcoj_phase_timing;
34pub use epistemic_workspace::{
35 EpistemicGpuBatchExecutionResult, EpistemicGpuBatchExecutionTrace,
36 EpistemicGpuCandidateGenerationTrace, EpistemicGpuCandidateValidationTrace,
37 EpistemicGpuConstraintValidationTrace, EpistemicGpuConstraintWorldViewValidationTrace,
38 EpistemicGpuExecutionResult, EpistemicGpuFinalResultMaterializationTrace,
39 EpistemicGpuFinalResultTransferTrace, EpistemicGpuFinalTupleMaterializationTrace,
40 EpistemicGpuKernelTimingTrace, EpistemicGpuMaterializationTrace,
41 EpistemicGpuModelMembershipSource, EpistemicGpuModelMembershipTrace,
42 EpistemicGpuPreparedExecution, EpistemicGpuPropagationTrace, EpistemicGpuProviderIdentity,
43 EpistemicGpuRejectionReason, EpistemicGpuRuntimeCounters, EpistemicGpuRuntimePreflight,
44 EpistemicGpuRuntimeTrace, EpistemicGpuRuntimeWcojCertification,
45 EpistemicGpuTransferBudgetTrace, EpistemicGpuWorkspace, EpistemicGpuWorkspaceCapacities,
46 EpistemicGpuWorkspaceLayout, EpistemicGpuWorkspaceResetTrace,
47 EpistemicGpuWorldViewValidationTrace,
48};
49use join_cache::JoinIndexCache;
50pub use join_cache::JoinIndexCacheStats;
51
52pub struct RelationDelta {
54 pub insert: Option<CudaBuffer>,
56 pub delete: Option<CudaBuffer>,
58}
59
60impl RelationDelta {
61 pub fn new(insert: Option<CudaBuffer>, delete: Option<CudaBuffer>) -> Self {
63 Self { insert, delete }
64 }
65}
66
67#[derive(Clone, Debug, Default, PartialEq, Eq)]
69pub struct DeltaRecomputeStats {
70 pub changed_relations: usize,
72 pub has_deletes: bool,
74 pub affected_sccs: usize,
76 pub recomputed_sccs: usize,
78 pub incremental_sccs: usize,
80}
81
82#[derive(Clone, Debug, Default, PartialEq, Eq)]
84pub struct CommonSubexpressionStats {
85 pub hits: u64,
87 pub misses: u64,
89 pub unsafe_rejections: u64,
91 pub rejection_reasons: Vec<String>,
93}
94
95#[derive(Clone, Debug, PartialEq)]
97pub struct AdaptiveJoinObservation {
98 pub left_rel: RelId,
100 pub right_rel: RelId,
102 pub estimated_output_rows: u64,
104 pub actual_output_rows: u64,
106 pub cardinality_delta_abs: u64,
108 pub estimated_selectivity: f64,
110 pub actual_selectivity: f64,
112 pub selectivity_delta_abs: f64,
114 pub left_heat: f32,
116 pub right_heat: f32,
118 pub heat_delta_abs: f32,
120 pub misplan_ratio: f64,
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126pub enum AdaptiveReoptimizationAction {
127 Disabled,
129 Skipped,
131 AttemptCandidate,
133 Adopted,
135 RolledBack,
137}
138
139#[derive(Clone, Debug, PartialEq)]
141pub struct AdaptiveReoptimizationDecision {
142 pub action: AdaptiveReoptimizationAction,
144 pub reason: String,
146 pub max_misplan_ratio: f64,
148 pub min_misplan_ratio: f64,
150}
151
152#[derive(Clone, Copy, Debug, PartialEq, Eq)]
154pub enum AdaptiveReoptimizationDiagnosticKind {
155 CandidateExecutionFailed,
157 CandidateOutputMismatch,
159 RollbackRestoredBaseline,
161}
162
163#[derive(Clone, Debug, PartialEq, Eq)]
165pub struct AdaptiveReoptimizationDiagnostic {
166 pub kind: AdaptiveReoptimizationDiagnosticKind,
168 pub message: String,
170}
171
172#[derive(Clone, Debug, Default, PartialEq)]
174pub struct AdaptiveReoptimizationStats {
175 pub invocations: u64,
177 pub disabled: u64,
179 pub skipped: u64,
181 pub adopted: u64,
183 pub rolled_back: u64,
185 pub last_decision: Option<AdaptiveReoptimizationDecision>,
187 pub last_observations: Vec<AdaptiveJoinObservation>,
189 pub diagnostics: Vec<AdaptiveReoptimizationDiagnostic>,
191 pub data_plane_dtoh_calls: u64,
193}
194
195#[derive(Clone, Debug, PartialEq, Eq, Hash)]
196enum CommonSubexpressionKey {
197 Scan {
198 rel: RelId,
199 generation: u64,
200 },
201 Filter {
202 input: Box<CommonSubexpressionKey>,
203 predicate: String,
204 },
205 Project {
206 input: Box<CommonSubexpressionKey>,
207 columns: Vec<String>,
208 },
209 Join {
210 left: Box<CommonSubexpressionKey>,
211 right: Box<CommonSubexpressionKey>,
212 left_keys: Vec<usize>,
213 right_keys: Vec<usize>,
214 },
215 Union {
216 inputs: Vec<CommonSubexpressionKey>,
217 },
218 Distinct {
219 input: Box<CommonSubexpressionKey>,
220 key_cols: Vec<usize>,
221 },
222}
223
224pub struct Executor {
244 provider: Arc<CudaKernelProvider>,
246 store: RelationStore,
248 rel_names: HashMap<RelId, String>,
250 name_to_rel: HashMap<String, RelId>,
252 stats: StatsManager,
254 join_index_cache: JoinIndexCache,
256 common_subexpression_cache: HashMap<CommonSubexpressionKey, CudaBuffer>,
258 common_subexpression_stats: CommonSubexpressionStats,
260 adaptive_reoptimization_stats: AdaptiveReoptimizationStats,
262 adaptive_join_observations: Vec<AdaptiveJoinObservation>,
264 config: RuntimeConfig,
266 profiler: Profiler,
268 ilp_registry: IlpRegistry,
270 ilp_last_result: Option<IlpTaggedResult>,
272 wcoj_triangle_dispatch_count: u64,
278 pub(super) wcoj_4cycle_dispatch_count: u64,
281 pub(super) chain_dispatch_count: u64,
284 pub(super) wcoj_clique5_dispatch_count: u64,
288 pub(super) wcoj_clique6_dispatch_count: u64,
291 pub(super) wcoj_clique7_dispatch_count: u64,
294 pub(super) wcoj_clique8_dispatch_count: u64,
297 pub(super) kclique_histogram_refresh_count: u64,
300 pub(super) kclique_histogram_refresh_nanos: u128,
303 pub(super) nested_loop_dispatch_count: u64,
312 pub(super) wcoj_error_decline_count: u64,
319 pub(super) wcoj_groupby_fusion_dispatch_count: u64,
323 pub(super) free_join_dispatch_count: u64,
327 pub(super) factorized_delta_dispatch_count: u64,
331 wcoj_dispatch_stream: OnceLock<xlog_cuda::device_runtime::StreamId>,
346 #[cfg(feature = "wcoj-phase-timing")]
353 pub(super) last_wcoj_phase_timing:
354 std::sync::Mutex<Option<wcoj_phase_timing::WcojDispatchPhaseTiming>>,
355 #[cfg(feature = "recursive-stats-trace")]
364 pub(super) last_recursive_stats_trace: RecursiveStatsTrace,
365}
366
367#[cfg(feature = "recursive-stats-trace")]
376#[derive(Debug, Default, Clone)]
377#[allow(missing_docs)]
378pub struct RecursiveStatsTrace {
379 pub entries: Vec<RecursiveStatsTraceEntry>,
380}
381
382#[cfg(feature = "recursive-stats-trace")]
390#[derive(Debug, Clone)]
391#[allow(missing_docs)]
392pub struct RecursiveStatsTraceEntry {
393 pub iteration: usize,
394 pub pred: String,
395 pub full_rel: RelId,
396 pub delta_rel: RelId,
397 pub full_rows: u64,
398 pub delta_rows: u64,
399 pub phase: RecursiveStatsPhase,
400 pub binary_est_for_variant: Option<u64>,
406}
407
408#[cfg(feature = "recursive-stats-trace")]
409#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410#[allow(missing_docs)]
411pub enum RecursiveStatsPhase {
412 Seed,
415 Phase2Delta,
419 Phase4Full,
423}
424
425impl Executor {
426 pub fn new(provider: Arc<CudaKernelProvider>) -> Self {
431 Self::new_with_config(provider, RuntimeConfig::default())
432 }
433
434 pub fn new_with_config(provider: Arc<CudaKernelProvider>, config: RuntimeConfig) -> Self {
436 const DEFAULT_JOIN_INDEX_CACHE_BYTES: u64 = 256 * 1024 * 1024;
437 let max_index_cache_bytes =
438 (provider.memory().budget().device_bytes / 4).min(DEFAULT_JOIN_INDEX_CACHE_BYTES);
439 Self {
440 provider: provider.clone(),
441 store: RelationStore::new(provider.clone()),
442 rel_names: HashMap::new(),
443 name_to_rel: HashMap::new(),
444 stats: StatsManager::new(),
445 join_index_cache: JoinIndexCache::new(max_index_cache_bytes),
446 common_subexpression_cache: HashMap::new(),
447 common_subexpression_stats: CommonSubexpressionStats::default(),
448 adaptive_reoptimization_stats: AdaptiveReoptimizationStats::default(),
449 adaptive_join_observations: Vec::new(),
450 config,
451 profiler: Profiler::default(),
452 ilp_registry: IlpRegistry::new(),
453 ilp_last_result: None,
454 wcoj_triangle_dispatch_count: 0,
455 wcoj_4cycle_dispatch_count: 0,
456 chain_dispatch_count: 0,
457 wcoj_clique5_dispatch_count: 0,
458 wcoj_clique6_dispatch_count: 0,
459 wcoj_clique7_dispatch_count: 0,
460 wcoj_clique8_dispatch_count: 0,
461 kclique_histogram_refresh_count: 0,
462 kclique_histogram_refresh_nanos: 0,
463 nested_loop_dispatch_count: 0,
464 wcoj_error_decline_count: 0,
465 wcoj_groupby_fusion_dispatch_count: 0,
466 free_join_dispatch_count: 0,
467 factorized_delta_dispatch_count: 0,
468 wcoj_dispatch_stream: OnceLock::new(),
469 #[cfg(feature = "wcoj-phase-timing")]
470 last_wcoj_phase_timing: std::sync::Mutex::new(None),
471 #[cfg(feature = "recursive-stats-trace")]
472 last_recursive_stats_trace: RecursiveStatsTrace::default(),
473 }
474 }
475
476 #[cfg(feature = "recursive-stats-trace")]
480 pub fn last_recursive_stats_trace(&self) -> &RecursiveStatsTrace {
481 &self.last_recursive_stats_trace
482 }
483
484 #[cfg(feature = "wcoj-phase-timing")]
493 pub fn take_wcoj_phase_timing(&self) -> Option<wcoj_phase_timing::WcojDispatchPhaseTiming> {
494 self.last_wcoj_phase_timing
495 .lock()
496 .ok()
497 .and_then(|mut g| g.take())
498 }
499
500 pub fn set_profiling(&mut self, enabled: bool) {
504 self.profiler = Profiler::new(enabled);
505 if enabled {
506 self.profiler
507 .set_memory_budget(self.provider.memory().budget().device_bytes);
508 }
509 }
510
511 pub fn is_profiling(&self) -> bool {
513 self.profiler.is_enabled()
514 }
515
516 pub fn execution_stats(&self, total_output_rows: u64) -> ExecutionStats {
520 let mut stats = self.profiler.execution_stats(total_output_rows);
521 stats.wcoj_triangle_dispatch_count = self.wcoj_triangle_dispatch_count();
526 stats.wcoj_4cycle_dispatch_count = self.wcoj_4cycle_dispatch_count();
527 stats.wcoj_groupby_fusion_dispatch_count = self.wcoj_groupby_fusion_dispatch_count();
528 stats.free_join_dispatch_count = self.free_join_dispatch_count();
529 stats.factorized_delta_dispatch_count = self.factorized_delta_dispatch_count();
530 stats.wcoj_error_decline_count = self.wcoj_error_decline_count();
531 stats
532 }
533
534 pub fn store(&self) -> &RelationStore {
536 &self.store
537 }
538
539 pub fn store_mut(&mut self) -> &mut RelationStore {
541 &mut self.store
542 }
543
544 pub fn ilp_registry_mut(&mut self) -> &mut IlpRegistry {
546 &mut self.ilp_registry
547 }
548
549 pub fn ilp_registry(&self) -> &IlpRegistry {
551 &self.ilp_registry
552 }
553
554 pub fn ilp_last_result(&self) -> Option<&IlpTaggedResult> {
556 self.ilp_last_result.as_ref()
557 }
558
559 pub fn put_relation(&mut self, name: &str, buffer: CudaBuffer) {
561 self.store_put(name, buffer);
562 }
563
564 pub fn stats(&self) -> &StatsManager {
566 &self.stats
567 }
568
569 pub fn join_index_cache_stats(&self) -> JoinIndexCacheStats {
571 self.join_index_cache.stats()
572 }
573
574 pub fn reset_for_mc(&mut self) {
578 self.store.clear();
579 self.join_index_cache.clear();
580 self.common_subexpression_cache.clear();
581 self.adaptive_join_observations.clear();
582 }
583
584 pub fn reset_for_mc_relations(
597 &mut self,
598 preserve: &[&str],
599 clear_to_empty: &[(&str, Schema)],
600 ) -> Result<()> {
601 let preserve_set: HashSet<&str> = preserve.iter().copied().collect();
602 let existing_names: Vec<String> = self.store.names().map(|s| s.to_string()).collect();
603
604 for name in &existing_names {
605 if !preserve_set.contains(name.as_str()) {
606 self.store.remove(name);
607 }
608 }
609
610 for (name, schema) in clear_to_empty {
611 let empty = self.provider.create_empty_buffer(schema.clone())?;
612 self.store.put(name, empty);
613 }
614
615 self.join_index_cache.clear();
616 self.common_subexpression_cache.clear();
617 self.adaptive_join_observations.clear();
618 Ok(())
619 }
620
621 pub fn reset_for_ilp(&mut self) {
628 self.ilp_registry.clear();
629 self.ilp_last_result = None;
630 self.store.clear();
631 self.join_index_cache.clear();
632 self.common_subexpression_cache.clear();
633 self.adaptive_join_observations.clear();
634 self.stats = StatsManager::new();
635 self.profiler = Profiler::default();
636 }
637
638 pub fn stats_mut(&mut self) -> &mut StatsManager {
640 &mut self.stats
641 }
642
643 pub fn stats_snapshot(&self) -> StatsSnapshot {
647 let mut snapshot = self.stats.snapshot();
648 snapshot.rel_names = self
649 .rel_names
650 .iter()
651 .map(|(id, name)| (*id, name.clone()))
652 .collect();
653 snapshot
654 }
655
656 pub fn common_subexpression_stats(&self) -> &CommonSubexpressionStats {
658 &self.common_subexpression_stats
659 }
660
661 pub fn adaptive_reoptimization_stats(&self) -> &AdaptiveReoptimizationStats {
663 &self.adaptive_reoptimization_stats
664 }
665
666 pub fn replay_adaptive_reoptimization_decision(
668 &self,
669 observations: &[AdaptiveJoinObservation],
670 ) -> AdaptiveReoptimizationDecision {
671 self.adaptive_reoptimization_decision(observations)
672 }
673
674 fn common_subexpression_enabled(&self) -> bool {
675 self.config.resolved_common_subexpression_elimination()
676 }
677
678 fn adaptive_reoptimization_enabled(&self) -> bool {
679 self.config.resolved_adaptive_reoptimization()
680 }
681
682 fn adaptive_reoptimization_decision(
683 &self,
684 observations: &[AdaptiveJoinObservation],
685 ) -> AdaptiveReoptimizationDecision {
686 let min_misplan_ratio = self
687 .config
688 .resolved_adaptive_reoptimization_min_misplan_ratio();
689 let max_misplan_ratio = observations
690 .iter()
691 .map(|observation| observation.misplan_ratio)
692 .fold(1.0_f64, f64::max);
693
694 if !self.adaptive_reoptimization_enabled() {
695 return AdaptiveReoptimizationDecision {
696 action: AdaptiveReoptimizationAction::Disabled,
697 reason: "adaptive_reoptimization_disabled".to_string(),
698 max_misplan_ratio,
699 min_misplan_ratio,
700 };
701 }
702
703 if observations.is_empty() {
704 return AdaptiveReoptimizationDecision {
705 action: AdaptiveReoptimizationAction::Skipped,
706 reason: "no_join_telemetry".to_string(),
707 max_misplan_ratio,
708 min_misplan_ratio,
709 };
710 }
711
712 if max_misplan_ratio >= min_misplan_ratio {
713 AdaptiveReoptimizationDecision {
714 action: AdaptiveReoptimizationAction::AttemptCandidate,
715 reason: "misplan_threshold_crossed".to_string(),
716 max_misplan_ratio,
717 min_misplan_ratio,
718 }
719 } else {
720 AdaptiveReoptimizationDecision {
721 action: AdaptiveReoptimizationAction::Skipped,
722 reason: "misplan_threshold_not_crossed".to_string(),
723 max_misplan_ratio,
724 min_misplan_ratio,
725 }
726 }
727 }
728
729 fn record_adaptive_join_observation(
730 &mut self,
731 left_rel: RelId,
732 right_rel: RelId,
733 left_keys: &[usize],
734 right_keys: &[usize],
735 input_rows: u64,
736 actual_output_rows: u64,
737 ) {
738 let estimated_output_rows = self
739 .stats
740 .estimate_join_cardinality(left_rel, right_rel, left_keys, right_keys);
741 let estimated_selectivity = if input_rows > 0 {
742 estimated_output_rows as f64 / input_rows as f64
743 } else {
744 0.0
745 };
746 let actual_selectivity = if input_rows > 0 {
747 actual_output_rows as f64 / input_rows as f64
748 } else {
749 0.0
750 };
751 let cardinality_delta_abs = estimated_output_rows.abs_diff(actual_output_rows);
752 let selectivity_delta_abs = (estimated_selectivity - actual_selectivity).abs();
753 let left_heat = self
754 .stats
755 .get_relation_stats(left_rel)
756 .map(|stats| stats.heat)
757 .unwrap_or(0.0);
758 let right_heat = self
759 .stats
760 .get_relation_stats(right_rel)
761 .map(|stats| stats.heat)
762 .unwrap_or(0.0);
763 let heat_delta_abs = (left_heat - right_heat).abs();
764 let smaller = estimated_output_rows.min(actual_output_rows);
765 let larger = estimated_output_rows.max(actual_output_rows);
766 let misplan_ratio = if smaller == 0 {
767 if larger == 0 {
768 1.0
769 } else {
770 f64::INFINITY
771 }
772 } else {
773 (larger as f64 / smaller as f64).max(1.0)
774 };
775
776 self.adaptive_join_observations
777 .push(AdaptiveJoinObservation {
778 left_rel,
779 right_rel,
780 estimated_output_rows,
781 actual_output_rows,
782 cardinality_delta_abs,
783 estimated_selectivity,
784 actual_selectivity,
785 selectivity_delta_abs,
786 left_heat,
787 right_heat,
788 heat_delta_abs,
789 misplan_ratio,
790 });
791 }
792
793 fn plan_head_names(plan: &ExecutionPlan) -> Vec<String> {
794 let mut names = Vec::new();
795 for stratum in &plan.strata {
796 for scc_id in &stratum.sccs {
797 if let Some(rules) = plan.rules_by_scc.get(*scc_id as usize) {
798 for rule in rules {
799 if !names.iter().any(|name| name == &rule.head) {
800 names.push(rule.head.clone());
801 }
802 }
803 }
804 }
805 }
806
807 if names.is_empty() {
808 for rules in &plan.rules_by_scc {
809 for rule in rules {
810 if !names.iter().any(|name| name == &rule.head) {
811 names.push(rule.head.clone());
812 }
813 }
814 }
815 }
816
817 names
818 }
819
820 fn clone_store_snapshot(&self) -> Result<HashMap<String, CudaBuffer>> {
821 let names: Vec<String> = self.store.names().map(|name| name.to_string()).collect();
822 let mut snapshot = HashMap::with_capacity(names.len());
823 for name in names {
824 if let Some(buffer) = self.store.get(&name) {
825 snapshot.insert(name, self.clone_buffer(buffer)?);
826 }
827 }
828 Ok(snapshot)
829 }
830
831 fn restore_store_snapshot(&mut self, snapshot: HashMap<String, CudaBuffer>) {
832 let snapshot_names: HashSet<String> = snapshot.keys().cloned().collect();
833 let existing_names: Vec<String> = self.store.names().map(|name| name.to_string()).collect();
834 for name in existing_names {
835 if !snapshot_names.contains(&name) {
836 self.store.remove(&name);
837 }
838 }
839 for (name, buffer) in snapshot {
840 self.store.put(&name, buffer);
841 }
842 }
843
844 fn restore_stats_snapshot(&mut self, snapshot: &StatsSnapshot) {
845 self.stats.clear();
846 self.stats.merge_snapshot(snapshot);
847 }
848
849 fn clone_final_plan_output(&self, plan: &ExecutionPlan) -> Result<CudaBuffer> {
850 let head_names = Self::plan_head_names(plan);
851 if let Some(name) = head_names.last() {
852 let output = self.store.get(name).ok_or_else(|| {
853 XlogError::Execution(format!("adaptive reoptimization output missing: {name}"))
854 })?;
855 return self.clone_buffer(output);
856 }
857
858 self.provider.create_empty_buffer(Schema::new(vec![]))
859 }
860
861 fn plan_outputs_match(
862 &self,
863 head_names: &[String],
864 baseline_snapshot: &HashMap<String, CudaBuffer>,
865 ) -> Result<bool> {
866 for name in head_names {
867 let Some(baseline) = baseline_snapshot.get(name) else {
868 return Ok(false);
869 };
870 let Some(candidate) = self.store.get(name) else {
871 return Ok(false);
872 };
873 if !self.buffers_gpu_set_equivalent(baseline, candidate)? {
874 return Ok(false);
875 }
876 }
877 Ok(true)
878 }
879
880 fn buffers_gpu_set_equivalent(&self, left: &CudaBuffer, right: &CudaBuffer) -> Result<bool> {
881 if left.schema() != right.schema() {
882 return Ok(false);
883 }
884 let left_rows = self.provider.device_row_count(left)?;
885 let right_rows = self.provider.device_row_count(right)?;
886 if left_rows != right_rows {
887 return Ok(false);
888 }
889
890 let left_minus_right = self.provider.diff_full_row(left, right)?;
891 if self.provider.device_row_count(&left_minus_right)? != 0 {
892 return Ok(false);
893 }
894 let right_minus_left = self.provider.diff_full_row(right, left)?;
895 Ok(self.provider.device_row_count(&right_minus_left)? == 0)
896 }
897
898 fn record_adaptive_dtoh_delta(&mut self, before_dtoh_calls: u64) {
899 let after_dtoh_calls = self.provider.host_transfer_stats().dtoh_calls;
900 self.adaptive_reoptimization_stats.data_plane_dtoh_calls =
901 after_dtoh_calls.saturating_sub(before_dtoh_calls);
902 }
903
904 fn is_common_subexpression_cacheable(node: &RirNode) -> bool {
905 !matches!(node, RirNode::Unit | RirNode::Scan { .. })
906 }
907
908 fn record_common_subexpression_rejection(&mut self, reason: &'static str) {
909 self.common_subexpression_stats.unsafe_rejections = self
910 .common_subexpression_stats
911 .unsafe_rejections
912 .saturating_add(1);
913 if !self
914 .common_subexpression_stats
915 .rejection_reasons
916 .iter()
917 .any(|seen| seen == reason)
918 {
919 self.common_subexpression_stats
920 .rejection_reasons
921 .push(reason.to_string());
922 }
923 }
924
925 fn common_subexpression_key(&mut self, node: &RirNode) -> Option<CommonSubexpressionKey> {
926 match node {
927 RirNode::Unit => None,
928 RirNode::Scan { rel } => {
929 let generation = self
930 .get_rel_name(*rel)
931 .and_then(|name| self.store.version(name))
932 .unwrap_or(0);
933 Some(CommonSubexpressionKey::Scan {
934 rel: *rel,
935 generation,
936 })
937 }
938 RirNode::Filter { input, predicate } => {
939 let input = self.common_subexpression_key(input)?;
940 Some(CommonSubexpressionKey::Filter {
941 input: Box::new(input),
942 predicate: Self::expr_cse_key(predicate),
943 })
944 }
945 RirNode::Project { input, columns } => {
946 let input = self.common_subexpression_key(input)?;
947 Some(CommonSubexpressionKey::Project {
948 input: Box::new(input),
949 columns: columns.iter().map(Self::project_expr_cse_key).collect(),
950 })
951 }
952 RirNode::Join {
953 left,
954 right,
955 left_keys,
956 right_keys,
957 join_type,
958 } => {
959 if *join_type != JoinType::Inner {
960 self.record_common_subexpression_rejection("negation_or_outer_join_boundary");
961 return None;
962 }
963 let left = self.common_subexpression_key(left)?;
964 let right = self.common_subexpression_key(right)?;
965 Some(CommonSubexpressionKey::Join {
966 left: Box::new(left),
967 right: Box::new(right),
968 left_keys: left_keys.clone(),
969 right_keys: right_keys.clone(),
970 })
971 }
972 RirNode::Union { inputs } => {
973 let mut input_keys = Vec::with_capacity(inputs.len());
974 for input in inputs {
975 input_keys.push(self.common_subexpression_key(input)?);
976 }
977 Some(CommonSubexpressionKey::Union { inputs: input_keys })
978 }
979 RirNode::Distinct { input, key_cols } => {
980 let input = self.common_subexpression_key(input)?;
981 Some(CommonSubexpressionKey::Distinct {
982 input: Box::new(input),
983 key_cols: key_cols.clone(),
984 })
985 }
986 RirNode::Diff { .. } => {
987 self.record_common_subexpression_rejection("negation_or_difference_boundary");
988 None
989 }
990 RirNode::GroupBy { .. } => {
991 self.record_common_subexpression_rejection("aggregate_boundary");
992 None
993 }
994 RirNode::Fixpoint { .. } => {
995 self.record_common_subexpression_rejection("recursive_or_mutable_boundary");
996 None
997 }
998 RirNode::TensorMaskedJoin { .. } => {
999 self.record_common_subexpression_rejection("provenance_or_tensor_boundary");
1000 None
1001 }
1002 RirNode::MultiWayJoin { .. } | RirNode::ChainJoin { .. } => {
1003 self.record_common_subexpression_rejection("specialized_dispatch_boundary");
1004 None
1005 }
1006 }
1007 }
1008
1009 fn expr_cse_key(expr: &Expr) -> String {
1010 match expr {
1011 Expr::Column(idx) => format!("col:{idx}"),
1012 Expr::Const(value) => format!("const:{}", Self::const_cse_key(value)),
1013 Expr::Compare { left, op, right } => format!(
1014 "cmp:{}:{}:{}",
1015 Self::expr_cse_key(left),
1016 Self::compare_op_cse_key(*op),
1017 Self::expr_cse_key(right)
1018 ),
1019 Expr::And(items) => format!(
1020 "and:[{}]",
1021 items
1022 .iter()
1023 .map(Self::expr_cse_key)
1024 .collect::<Vec<_>>()
1025 .join(",")
1026 ),
1027 Expr::Or(items) => format!(
1028 "or:[{}]",
1029 items
1030 .iter()
1031 .map(Self::expr_cse_key)
1032 .collect::<Vec<_>>()
1033 .join(",")
1034 ),
1035 Expr::Not(inner) => format!("not:{}", Self::expr_cse_key(inner)),
1036 Expr::Add(left, right) => {
1037 format!(
1038 "add:{}:{}",
1039 Self::expr_cse_key(left),
1040 Self::expr_cse_key(right)
1041 )
1042 }
1043 Expr::Sub(left, right) => {
1044 format!(
1045 "sub:{}:{}",
1046 Self::expr_cse_key(left),
1047 Self::expr_cse_key(right)
1048 )
1049 }
1050 Expr::Mul(left, right) => {
1051 format!(
1052 "mul:{}:{}",
1053 Self::expr_cse_key(left),
1054 Self::expr_cse_key(right)
1055 )
1056 }
1057 Expr::Div(left, right) => {
1058 format!(
1059 "div:{}:{}",
1060 Self::expr_cse_key(left),
1061 Self::expr_cse_key(right)
1062 )
1063 }
1064 Expr::Mod(left, right) => {
1065 format!(
1066 "mod:{}:{}",
1067 Self::expr_cse_key(left),
1068 Self::expr_cse_key(right)
1069 )
1070 }
1071 Expr::Abs(inner) => format!("abs:{}", Self::expr_cse_key(inner)),
1072 Expr::Min(left, right) => {
1073 format!(
1074 "min:{}:{}",
1075 Self::expr_cse_key(left),
1076 Self::expr_cse_key(right)
1077 )
1078 }
1079 Expr::Max(left, right) => {
1080 format!(
1081 "max:{}:{}",
1082 Self::expr_cse_key(left),
1083 Self::expr_cse_key(right)
1084 )
1085 }
1086 Expr::Pow(left, right) => {
1087 format!(
1088 "pow:{}:{}",
1089 Self::expr_cse_key(left),
1090 Self::expr_cse_key(right)
1091 )
1092 }
1093 Expr::Cast(inner, ty) => format!("cast:{:?}:{}", ty, Self::expr_cse_key(inner)),
1094 Expr::Conditional {
1095 condition,
1096 then_expr,
1097 else_expr,
1098 } => format!(
1099 "if:{}:{}:{}",
1100 Self::expr_cse_key(condition),
1101 Self::expr_cse_key(then_expr),
1102 Self::expr_cse_key(else_expr)
1103 ),
1104 }
1105 }
1106
1107 fn project_expr_cse_key(expr: &ProjectExpr) -> String {
1108 match expr {
1109 ProjectExpr::Column(idx) => format!("col:{idx}"),
1110 ProjectExpr::Computed(expr, ty) => {
1111 format!("computed:{:?}:{}", ty, Self::expr_cse_key(expr))
1112 }
1113 }
1114 }
1115
1116 fn const_cse_key(value: &xlog_ir::ConstValue) -> String {
1117 match value {
1118 xlog_ir::ConstValue::U32(value) => format!("u32:{value}"),
1119 xlog_ir::ConstValue::U64(value) => format!("u64:{value}"),
1120 xlog_ir::ConstValue::I32(value) => format!("i32:{value}"),
1121 xlog_ir::ConstValue::I64(value) => format!("i64:{value}"),
1122 xlog_ir::ConstValue::F32(value) => format!("f32:{:08x}", value.to_bits()),
1123 xlog_ir::ConstValue::F64(value) => format!("f64:{:016x}", value.to_bits()),
1124 xlog_ir::ConstValue::Bool(value) => format!("bool:{value}"),
1125 xlog_ir::ConstValue::Symbol(value) => format!("symbol:{value:?}"),
1126 }
1127 }
1128
1129 fn compare_op_cse_key(op: xlog_ir::CompareOp) -> &'static str {
1130 match op {
1131 xlog_ir::CompareOp::Eq => "eq",
1132 xlog_ir::CompareOp::Ne => "ne",
1133 xlog_ir::CompareOp::Lt => "lt",
1134 xlog_ir::CompareOp::Le => "le",
1135 xlog_ir::CompareOp::Gt => "gt",
1136 xlog_ir::CompareOp::Ge => "ge",
1137 }
1138 }
1139
1140 fn store_put(&mut self, name: &str, buffer: CudaBuffer) {
1141 self.common_subexpression_cache.clear();
1142 self.store.put(name, buffer);
1143 if let Some(&rel_id) = self.name_to_rel.get(name) {
1144 self.join_index_cache.invalidate_rel(rel_id);
1145 }
1146 }
1147
1148 fn store_remove(&mut self, name: &str) -> Option<CudaBuffer> {
1149 self.common_subexpression_cache.clear();
1150 if let Some(&rel_id) = self.name_to_rel.get(name) {
1151 self.join_index_cache.invalidate_rel(rel_id);
1152 }
1153 self.store.remove(name)
1154 }
1155
1156 pub fn register_relation(&mut self, rel_id: RelId, name: &str) {
1165 self.rel_names.insert(rel_id, name.to_string());
1166 self.name_to_rel.insert(name.to_string(), rel_id);
1167 self.stats.register_relation(rel_id);
1168 }
1169
1170 fn name_to_rel_id(&self, name: &str) -> Option<RelId> {
1178 self.name_to_rel.get(name).copied()
1179 }
1180
1181 fn get_rel_name(&self, rel_id: RelId) -> Option<&str> {
1183 self.rel_names.get(&rel_id).map(|s| s.as_str())
1184 }
1185
1186 pub fn execute_plan_with_adaptive_candidate(
1196 &mut self,
1197 baseline_plan: &ExecutionPlan,
1198 candidate_plan: &ExecutionPlan,
1199 ) -> Result<CudaBuffer> {
1200 self.adaptive_reoptimization_stats.invocations = self
1201 .adaptive_reoptimization_stats
1202 .invocations
1203 .saturating_add(1);
1204 self.adaptive_reoptimization_stats.diagnostics.clear();
1205 let before_dtoh_calls = self.provider.host_transfer_stats().dtoh_calls;
1206
1207 self.execute_plan(baseline_plan)?;
1208 let baseline_observations = self.adaptive_join_observations.clone();
1209 self.adaptive_reoptimization_stats.last_observations = baseline_observations.clone();
1210 let decision = self.adaptive_reoptimization_decision(&baseline_observations);
1211 self.adaptive_reoptimization_stats.last_decision = Some(decision.clone());
1212
1213 match decision.action {
1214 AdaptiveReoptimizationAction::Disabled => {
1215 self.adaptive_reoptimization_stats.disabled = self
1216 .adaptive_reoptimization_stats
1217 .disabled
1218 .saturating_add(1);
1219 self.record_adaptive_dtoh_delta(before_dtoh_calls);
1220 return self.clone_final_plan_output(baseline_plan);
1221 }
1222 AdaptiveReoptimizationAction::Skipped => {
1223 self.adaptive_reoptimization_stats.skipped =
1224 self.adaptive_reoptimization_stats.skipped.saturating_add(1);
1225 self.record_adaptive_dtoh_delta(before_dtoh_calls);
1226 return self.clone_final_plan_output(baseline_plan);
1227 }
1228 AdaptiveReoptimizationAction::AttemptCandidate => {}
1229 AdaptiveReoptimizationAction::Adopted | AdaptiveReoptimizationAction::RolledBack => {
1230 unreachable!("decision replay never returns terminal adaptive actions")
1231 }
1232 }
1233
1234 let head_names = Self::plan_head_names(baseline_plan);
1235 let baseline_snapshot = self.clone_store_snapshot()?;
1236 let baseline_stats_snapshot = self.stats_snapshot();
1237
1238 if let Err(err) = self.execute_plan(candidate_plan) {
1239 self.restore_store_snapshot(baseline_snapshot);
1240 self.restore_stats_snapshot(&baseline_stats_snapshot);
1241 self.adaptive_reoptimization_stats.rolled_back = self
1242 .adaptive_reoptimization_stats
1243 .rolled_back
1244 .saturating_add(1);
1245 self.adaptive_reoptimization_stats
1246 .diagnostics
1247 .push(AdaptiveReoptimizationDiagnostic {
1248 kind: AdaptiveReoptimizationDiagnosticKind::CandidateExecutionFailed,
1249 message: err.to_string(),
1250 });
1251 self.adaptive_reoptimization_stats
1252 .diagnostics
1253 .push(AdaptiveReoptimizationDiagnostic {
1254 kind: AdaptiveReoptimizationDiagnosticKind::RollbackRestoredBaseline,
1255 message: "baseline_snapshot_restored".to_string(),
1256 });
1257 self.adaptive_reoptimization_stats.last_observations = baseline_observations;
1258 self.adaptive_reoptimization_stats.last_decision =
1259 Some(AdaptiveReoptimizationDecision {
1260 action: AdaptiveReoptimizationAction::RolledBack,
1261 reason: "candidate_execution_failed".to_string(),
1262 max_misplan_ratio: decision.max_misplan_ratio,
1263 min_misplan_ratio: decision.min_misplan_ratio,
1264 });
1265 self.record_adaptive_dtoh_delta(before_dtoh_calls);
1266 return self.clone_final_plan_output(baseline_plan);
1267 }
1268
1269 if !self.plan_outputs_match(&head_names, &baseline_snapshot)? {
1270 self.restore_store_snapshot(baseline_snapshot);
1271 self.restore_stats_snapshot(&baseline_stats_snapshot);
1272 self.adaptive_reoptimization_stats.rolled_back = self
1273 .adaptive_reoptimization_stats
1274 .rolled_back
1275 .saturating_add(1);
1276 self.adaptive_reoptimization_stats
1277 .diagnostics
1278 .push(AdaptiveReoptimizationDiagnostic {
1279 kind: AdaptiveReoptimizationDiagnosticKind::CandidateOutputMismatch,
1280 message: "candidate_output_mismatch".to_string(),
1281 });
1282 self.adaptive_reoptimization_stats
1283 .diagnostics
1284 .push(AdaptiveReoptimizationDiagnostic {
1285 kind: AdaptiveReoptimizationDiagnosticKind::RollbackRestoredBaseline,
1286 message: "baseline_snapshot_restored".to_string(),
1287 });
1288 self.adaptive_reoptimization_stats.last_observations = baseline_observations;
1289 self.adaptive_reoptimization_stats.last_decision =
1290 Some(AdaptiveReoptimizationDecision {
1291 action: AdaptiveReoptimizationAction::RolledBack,
1292 reason: "candidate_output_mismatch".to_string(),
1293 max_misplan_ratio: decision.max_misplan_ratio,
1294 min_misplan_ratio: decision.min_misplan_ratio,
1295 });
1296 self.record_adaptive_dtoh_delta(before_dtoh_calls);
1297 return self.clone_final_plan_output(baseline_plan);
1298 }
1299
1300 self.adaptive_reoptimization_stats.adopted =
1301 self.adaptive_reoptimization_stats.adopted.saturating_add(1);
1302 self.adaptive_reoptimization_stats.last_observations = baseline_observations;
1303 self.adaptive_reoptimization_stats.last_decision = Some(AdaptiveReoptimizationDecision {
1304 action: AdaptiveReoptimizationAction::Adopted,
1305 reason: "candidate_adopted".to_string(),
1306 max_misplan_ratio: decision.max_misplan_ratio,
1307 min_misplan_ratio: decision.min_misplan_ratio,
1308 });
1309 self.record_adaptive_dtoh_delta(before_dtoh_calls);
1310 self.clone_final_plan_output(candidate_plan)
1311 }
1312
1313 pub fn execute_plan(&mut self, plan: &ExecutionPlan) -> Result<CudaBuffer> {
1327 self.adaptive_join_observations.clear();
1328 self.common_subexpression_cache.clear();
1329 let gate = self.config.strict_deterministic_d2h;
1336 let prev_gate = self.provider.strict_deterministic_d2h_enabled();
1337 if gate && !prev_gate {
1338 self.provider.reset_deterministic_d2h_violations();
1343 self.provider.enable_strict_deterministic_d2h();
1344 }
1345 let _gate_guard = D2hGateGuard {
1348 provider: Arc::clone(&self.provider),
1349 engaged: gate,
1350 previous: prev_gate,
1351 };
1352
1353 for (idx, stratum) in plan.strata.iter().enumerate() {
1355 let (num_rules, is_recursive) = stratum
1357 .sccs
1358 .iter()
1359 .map(|&scc_id| {
1360 let rules = plan
1361 .rules_by_scc
1362 .get(scc_id as usize)
1363 .map(|r| r.len())
1364 .unwrap_or(0);
1365 let recursive = plan
1366 .sccs
1367 .get(scc_id as usize)
1368 .map(|s| s.is_recursive)
1369 .unwrap_or(false);
1370 (rules, recursive)
1371 })
1372 .fold((0, false), |(r, rec), (nr, nrec)| (r + nr, rec || nrec));
1373
1374 self.profiler.begin_stratum(idx, num_rules, is_recursive);
1375 self.execute_stratum_impl(stratum, plan)?;
1376
1377 let mem_bytes = self.provider.memory().allocated_bytes();
1379 self.profiler.record_peak_memory(mem_bytes);
1380
1381 self.profiler.end_stratum();
1382 }
1383
1384 self.provider.device().synchronize()?;
1386 self.adaptive_reoptimization_stats.last_observations =
1387 self.adaptive_join_observations.clone();
1388
1389 self.provider.create_empty_buffer(Schema::new(vec![]))
1391 }
1392
1393 #[cfg(test)]
1405 fn evaluate_predicate(
1406 expr: &Expr,
1407 columns: &[Vec<u8>],
1408 row_idx: usize,
1409 schema: &Schema,
1410 ) -> Result<bool> {
1411 match expr {
1412 Expr::Column(col_idx) => {
1413 let col_type = schema.column_type(*col_idx);
1415 if let Some(ScalarType::Bool) = col_type {
1416 Ok(columns
1417 .get(*col_idx)
1418 .map(|c| c.get(row_idx).copied().unwrap_or(0) != 0)
1419 .unwrap_or(false))
1420 } else {
1421 Ok(true)
1423 }
1424 }
1425
1426 Expr::Const(ConstValue::Bool(b)) => Ok(*b),
1427 Expr::Const(_) => Ok(true), Expr::Compare { left, op, right } => {
1430 let use_float =
1431 Self::expr_may_be_float(left, schema) || Self::expr_may_be_float(right, schema);
1432
1433 if use_float {
1434 let left_val = Self::evaluate_expr_as_f64(left, columns, row_idx, schema)?;
1435 let right_val = Self::evaluate_expr_as_f64(right, columns, row_idx, schema)?;
1436
1437 Ok(match op {
1438 CompareOp::Eq => left_val == right_val,
1439 CompareOp::Ne => left_val != right_val,
1440 CompareOp::Lt => left_val < right_val,
1441 CompareOp::Le => left_val <= right_val,
1442 CompareOp::Gt => left_val > right_val,
1443 CompareOp::Ge => left_val >= right_val,
1444 })
1445 } else {
1446 let left_val = Self::evaluate_expr_as_i64(left, columns, row_idx, schema)?;
1447 let right_val = Self::evaluate_expr_as_i64(right, columns, row_idx, schema)?;
1448
1449 Ok(match op {
1450 CompareOp::Eq => left_val == right_val,
1451 CompareOp::Ne => left_val != right_val,
1452 CompareOp::Lt => left_val < right_val,
1453 CompareOp::Le => left_val <= right_val,
1454 CompareOp::Gt => left_val > right_val,
1455 CompareOp::Ge => left_val >= right_val,
1456 })
1457 }
1458 }
1459
1460 Expr::And(exprs) => {
1461 for e in exprs {
1462 if !Self::evaluate_predicate(e, columns, row_idx, schema)? {
1463 return Ok(false);
1464 }
1465 }
1466 Ok(true)
1467 }
1468
1469 Expr::Or(exprs) => {
1470 for e in exprs {
1471 if Self::evaluate_predicate(e, columns, row_idx, schema)? {
1472 return Ok(true);
1473 }
1474 }
1475 Ok(false)
1476 }
1477
1478 Expr::Not(inner) => Ok(!Self::evaluate_predicate(inner, columns, row_idx, schema)?),
1479
1480 Expr::Add(_, _)
1482 | Expr::Sub(_, _)
1483 | Expr::Mul(_, _)
1484 | Expr::Div(_, _)
1485 | Expr::Mod(_, _)
1486 | Expr::Abs(_)
1487 | Expr::Min(_, _)
1488 | Expr::Max(_, _)
1489 | Expr::Pow(_, _)
1490 | Expr::Cast(_, _)
1491 | Expr::Conditional { .. } => Err(XlogError::Execution(
1492 "Arithmetic expression cannot be evaluated as boolean predicate".into(),
1493 )),
1494 }
1495 }
1496
1497 #[cfg(test)]
1498 fn evaluate_expr_as_f64(
1499 expr: &Expr,
1500 columns: &[Vec<u8>],
1501 row_idx: usize,
1502 schema: &Schema,
1503 ) -> Result<f64> {
1504 match expr {
1505 Expr::Column(col_idx) => {
1506 let col_type = schema.column_type(*col_idx).unwrap_or(ScalarType::U32);
1507 let col_data = columns
1508 .get(*col_idx)
1509 .ok_or_else(|| XlogError::Execution(format!("Column {} not found", col_idx)))?;
1510
1511 let type_size = col_type.size_bytes();
1512 let start = row_idx * type_size;
1513
1514 Ok(match col_type {
1515 ScalarType::F64 => {
1516 let bytes = &col_data[start..start + 8];
1517 f64::from_le_bytes([
1518 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1519 bytes[7],
1520 ])
1521 }
1522 ScalarType::F32 => {
1523 let bytes = &col_data[start..start + 4];
1524 f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64
1525 }
1526 ScalarType::U32 | ScalarType::Symbol => {
1527 let bytes = &col_data[start..start + 4];
1528 u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64
1529 }
1530 ScalarType::I32 => {
1531 let bytes = &col_data[start..start + 4];
1532 i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64
1533 }
1534 ScalarType::U64 => {
1535 let bytes = &col_data[start..start + 8];
1536 u64::from_le_bytes([
1537 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1538 bytes[7],
1539 ]) as f64
1540 }
1541 ScalarType::I64 => {
1542 let bytes = &col_data[start..start + 8];
1543 i64::from_le_bytes([
1544 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1545 bytes[7],
1546 ]) as f64
1547 }
1548 ScalarType::Bool => col_data.get(start).copied().unwrap_or(0) as f64,
1549 })
1550 }
1551
1552 Expr::Const(val) => Ok(match val {
1553 ConstValue::U32(v) => *v as f64,
1554 ConstValue::I32(v) => *v as f64,
1555 ConstValue::U64(v) => *v as f64,
1556 ConstValue::I64(v) => *v as f64,
1557 ConstValue::Bool(b) => {
1558 if *b {
1559 1.0
1560 } else {
1561 0.0
1562 }
1563 }
1564 ConstValue::F32(f) => *f as f64,
1565 ConstValue::F64(f) => *f,
1566 ConstValue::Symbol(_) => {
1567 return Err(XlogError::Execution(
1568 "Cannot evaluate Symbol constant as f64".to_string(),
1569 ));
1570 }
1571 }),
1572
1573 Expr::Add(l, r) => Ok(Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?
1574 + Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?),
1575 Expr::Sub(l, r) => Ok(Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?
1576 - Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?),
1577 Expr::Mul(l, r) => Ok(Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?
1578 * Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?),
1579 Expr::Div(l, r) => {
1580 let left_val = Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?;
1581 let right_val = Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?;
1582 if right_val == 0.0 {
1583 return Err(XlogError::Execution("Division by zero".to_string()));
1584 }
1585 Ok(left_val / right_val)
1586 }
1587 Expr::Mod(l, r) => {
1588 let left_val = Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?;
1589 let right_val = Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?;
1590 if right_val == 0.0 {
1591 return Err(XlogError::Execution("Modulo by zero".to_string()));
1592 }
1593 Ok(left_val % right_val)
1594 }
1595 Expr::Abs(inner) => {
1596 Ok(Self::evaluate_expr_as_f64(inner, columns, row_idx, schema)?.abs())
1597 }
1598 Expr::Min(l, r) => Ok(Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?
1599 .min(Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?)),
1600 Expr::Max(l, r) => Ok(Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?
1601 .max(Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?)),
1602 Expr::Pow(base, exp) => Ok(Self::evaluate_expr_as_f64(base, columns, row_idx, schema)?
1603 .powf(Self::evaluate_expr_as_f64(exp, columns, row_idx, schema)?)),
1604 Expr::Cast(inner, target_type) => match target_type {
1605 ScalarType::F64 => Self::evaluate_expr_as_f64(inner, columns, row_idx, schema),
1606 ScalarType::F32 => {
1607 Ok(Self::evaluate_expr_as_f64(inner, columns, row_idx, schema)? as f32 as f64)
1608 }
1609 _ => Ok(Self::evaluate_expr_as_i64(inner, columns, row_idx, schema)? as f64),
1610 },
1611
1612 _ => Err(XlogError::Execution(
1613 "Cannot evaluate compound expression as f64".to_string(),
1614 )),
1615 }
1616 }
1617
1618 #[cfg(test)]
1620 fn evaluate_expr_as_i64(
1621 expr: &Expr,
1622 columns: &[Vec<u8>],
1623 row_idx: usize,
1624 schema: &Schema,
1625 ) -> Result<i64> {
1626 match expr {
1627 Expr::Column(col_idx) => {
1628 let col_type = schema.column_type(*col_idx).unwrap_or(ScalarType::U32);
1629 let col_data = columns
1630 .get(*col_idx)
1631 .ok_or_else(|| XlogError::Execution(format!("Column {} not found", col_idx)))?;
1632
1633 let type_size = col_type.size_bytes();
1634 let start = row_idx * type_size;
1635
1636 Ok(match col_type {
1637 ScalarType::U32 => {
1638 let bytes = &col_data[start..start + 4];
1639 u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as i64
1640 }
1641 ScalarType::I32 => {
1642 let bytes = &col_data[start..start + 4];
1643 i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as i64
1644 }
1645 ScalarType::U64 => {
1646 let bytes = &col_data[start..start + 8];
1647 u64::from_le_bytes([
1648 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1649 bytes[7],
1650 ]) as i64
1651 }
1652 ScalarType::I64 => {
1653 let bytes = &col_data[start..start + 8];
1654 i64::from_le_bytes([
1655 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1656 bytes[7],
1657 ])
1658 }
1659 ScalarType::Bool => col_data.get(start).copied().unwrap_or(0) as i64,
1660 ScalarType::Symbol => {
1661 let bytes = &col_data[start..start + 4];
1662 u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as i64
1663 }
1664 ScalarType::F32 => {
1665 let bytes = &col_data[start..start + 4];
1666 let val = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1667 val as i64
1668 }
1669 ScalarType::F64 => {
1670 let bytes = &col_data[start..start + 8];
1671 let val = f64::from_le_bytes([
1672 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1673 bytes[7],
1674 ]);
1675 val as i64
1676 }
1677 })
1678 }
1679
1680 Expr::Const(val) => Ok(match val {
1681 ConstValue::U32(v) => *v as i64,
1682 ConstValue::I32(v) => *v as i64,
1683 ConstValue::U64(v) => *v as i64,
1684 ConstValue::I64(v) => *v,
1685 ConstValue::Bool(b) => *b as i64,
1686 ConstValue::F32(f) => *f as i64,
1687 ConstValue::F64(f) => *f as i64,
1688 ConstValue::Symbol(_) => 0,
1689 }),
1690
1691 Expr::Add(l, r) => {
1693 let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1694 let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1695 Ok(left_val.wrapping_add(right_val))
1696 }
1697 Expr::Sub(l, r) => {
1698 let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1699 let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1700 Ok(left_val.wrapping_sub(right_val))
1701 }
1702 Expr::Mul(l, r) => {
1703 let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1704 let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1705 Ok(left_val.wrapping_mul(right_val))
1706 }
1707 Expr::Div(l, r) => {
1708 let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1709 let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1710 if right_val == 0 {
1711 return Err(XlogError::Execution("Division by zero".to_string()));
1712 }
1713 Ok(left_val / right_val)
1714 }
1715 Expr::Mod(l, r) => {
1716 let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1717 let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1718 if right_val == 0 {
1719 return Err(XlogError::Execution("Modulo by zero".to_string()));
1720 }
1721 Ok(left_val % right_val)
1722 }
1723 Expr::Abs(inner) => {
1724 let val = Self::evaluate_expr_as_i64(inner, columns, row_idx, schema)?;
1725 Ok(val.abs())
1726 }
1727 Expr::Min(l, r) => {
1728 let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1729 let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1730 Ok(left_val.min(right_val))
1731 }
1732 Expr::Max(l, r) => {
1733 let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1734 let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1735 Ok(left_val.max(right_val))
1736 }
1737 Expr::Pow(base, exp) => {
1738 let base_val = Self::evaluate_expr_as_i64(base, columns, row_idx, schema)?;
1739 let exp_val = Self::evaluate_expr_as_i64(exp, columns, row_idx, schema)?;
1740 if exp_val < 0 {
1741 Err(XlogError::Execution(
1742 "Negative exponent in integer pow".to_string(),
1743 ))
1744 } else if exp_val > u32::MAX as i64 {
1745 Ok(i64::MAX)
1747 } else {
1748 Ok(base_val.pow(exp_val as u32))
1749 }
1750 }
1751 Expr::Cast(inner, _target_type) => {
1752 Self::evaluate_expr_as_i64(inner, columns, row_idx, schema)
1754 }
1755
1756 _ => Err(XlogError::Execution(
1757 "Cannot evaluate compound expression as value".to_string(),
1758 )),
1759 }
1760 }
1761
1762 fn get_or_create_rel_name(&mut self, rel_id: RelId, default: &str) -> String {
1764 if let Some(name) = self.rel_names.get(&rel_id) {
1765 name.clone()
1766 } else {
1767 self.register_relation(rel_id, default);
1768 default.to_string()
1769 }
1770 }
1771
1772 fn create_empty_buffer(&self, schema: Schema) -> Result<CudaBuffer> {
1776 self.provider.create_empty_buffer(schema)
1777 }
1778
1779 fn clone_buffer(&self, buffer: &CudaBuffer) -> Result<CudaBuffer> {
1781 if buffer.is_empty() {
1782 return self.create_empty_buffer(buffer.schema().clone());
1783 }
1784
1785 let mut result_columns = Vec::with_capacity(buffer.arity());
1786
1787 for col_idx in 0..buffer.arity() {
1788 let col_type_size = buffer
1789 .schema()
1790 .column_type(col_idx)
1791 .map(|t| t.size_bytes())
1792 .unwrap_or(4);
1793 let bytes = (buffer.num_rows() as usize) * col_type_size;
1794
1795 if let Some(src_col) = buffer.column(col_idx) {
1796 let mut dst_col = self.provider.memory().alloc::<u8>(bytes)?;
1797 if bytes > 0 {
1798 self.provider
1799 .device()
1800 .inner()
1801 .dtod_copy(src_col, &mut dst_col)
1802 .map_err(|e| {
1803 XlogError::Execution(format!("Failed to clone column on device: {}", e))
1804 })?;
1805 }
1806 result_columns.push(dst_col.into());
1807 }
1808 }
1809
1810 let d_num_rows = self.clone_device_row_count(buffer)?;
1811 Ok(CudaBuffer::from_columns(
1812 result_columns,
1813 buffer.num_rows(),
1814 d_num_rows,
1815 buffer.schema().clone(),
1816 ))
1817 }
1818
1819 fn clone_device_row_count(&self, buffer: &CudaBuffer) -> Result<TrackedCudaSlice<u32>> {
1820 let mut d_num_rows = self.provider.memory().alloc::<u32>(1)?;
1821 self.provider
1822 .device()
1823 .inner()
1824 .dtod_copy(buffer.num_rows_device(), &mut d_num_rows)
1825 .map_err(|e| XlogError::Execution(format!("Failed to copy row count: {}", e)))?;
1826 Ok(d_num_rows)
1827 }
1828
1829 fn buffer_row_count(&self, buffer: &CudaBuffer) -> Result<u32> {
1830 if let Some(n) = buffer.cached_row_count() {
1831 return Ok(n);
1832 }
1833 let n = self
1841 .provider
1842 .dtoh_scalar_untracked::<u32>(buffer.num_rows_device(), 0)
1843 .map_err(|e| XlogError::Execution(format!("Failed to read row count: {}", e)))?;
1844 buffer.set_cached_row_count_if_unset(n);
1845 Ok(n)
1846 }
1847}
1848
1849struct D2hGateGuard {
1853 provider: Arc<CudaKernelProvider>,
1854 engaged: bool,
1855 previous: bool,
1856}
1857
1858impl Drop for D2hGateGuard {
1859 fn drop(&mut self) {
1860 if !self.engaged {
1861 return;
1862 }
1863 if self.previous {
1864 self.provider.enable_strict_deterministic_d2h();
1865 } else {
1866 self.provider.disable_strict_deterministic_d2h();
1867 }
1868 }
1869}
1870
1871#[cfg(test)]
1872mod tests {
1873 use super::*;
1874 use std::time::{Duration, Instant};
1875 use xlog_core::MemoryBudget;
1876 use xlog_cuda::{CudaDevice, GpuMemoryManager};
1877 use xlog_ir::{CompiledRule, RirMeta, Scc};
1878
1879 fn has_cuda_device() -> bool {
1880 CudaDevice::new(0).is_ok()
1882 }
1883
1884 fn create_test_executor() -> Option<Executor> {
1885 if !has_cuda_device() {
1886 return None;
1887 }
1888 let device = Arc::new(CudaDevice::new(0).ok()?);
1889 let budget = MemoryBudget::with_limit(1024 * 1024 * 1024); let memory = Arc::new(GpuMemoryManager::new(device.clone(), budget));
1891 let provider = Arc::new(CudaKernelProvider::new(device, memory).ok()?);
1892 Some(Executor::new(provider))
1893 }
1894
1895 fn create_test_executor_with_config(config: RuntimeConfig) -> Option<Executor> {
1896 if !has_cuda_device() {
1897 return None;
1898 }
1899 let device = Arc::new(CudaDevice::new(0).ok()?);
1900 let budget = MemoryBudget::with_limit(1024 * 1024 * 1024); let memory = Arc::new(GpuMemoryManager::new(device.clone(), budget));
1902 let provider = Arc::new(CudaKernelProvider::new(device, memory).ok()?);
1903 Some(Executor::new_with_config(provider, config))
1904 }
1905
1906 fn device_row_count(executor: &Executor, rows: u64) -> TrackedCudaSlice<u32> {
1907 let rows_u32 = u32::try_from(rows).expect("row count fits u32");
1908 let mut d_num_rows = executor.provider.memory().alloc::<u32>(1).expect("alloc");
1909 executor
1910 .provider
1911 .device()
1912 .inner()
1913 .htod_sync_copy_into(&[rows_u32], &mut d_num_rows)
1914 .expect("htod");
1915 d_num_rows
1916 }
1917
1918 fn create_test_buffer(executor: &Executor, data: &[u32], col_name: &str) -> CudaBuffer {
1919 let schema = Schema::new(vec![(col_name.to_string(), ScalarType::U32)]);
1920 let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_le_bytes()).collect();
1921
1922 let mut col = executor
1923 .provider
1924 .memory()
1925 .alloc::<u8>(bytes.len())
1926 .expect("alloc");
1927 executor
1928 .provider
1929 .device()
1930 .inner()
1931 .htod_sync_copy_into(&bytes, &mut col)
1932 .expect("htod");
1933
1934 let rows = data.len() as u64;
1935 let d_num_rows = device_row_count(executor, rows);
1936 CudaBuffer::from_columns(vec![col.into()], rows, d_num_rows, schema)
1937 }
1938
1939 fn read_buffer_u32(executor: &Executor, buffer: &CudaBuffer, col: usize) -> Vec<u32> {
1940 executor
1941 .provider
1942 .download_column::<u32>(buffer, col)
1943 .unwrap_or_default()
1944 }
1945
1946 fn buffer_row_count(executor: &Executor, buffer: &CudaBuffer) -> u32 {
1947 if let Some(n) = buffer.cached_row_count() {
1948 return n;
1949 }
1950 let mut host_rows = [0u32];
1951 executor
1952 .provider
1953 .device()
1954 .inner()
1955 .dtoh_sync_copy_into(buffer.num_rows_device(), &mut host_rows)
1956 .expect("dtoh row count");
1957 buffer.set_cached_row_count_if_unset(host_rows[0]);
1958 host_rows[0]
1959 }
1960
1961 fn to_f64_column_bytes(values: &[f64]) -> Vec<u8> {
1962 values.iter().flat_map(|v| v.to_le_bytes()).collect()
1963 }
1964
1965 fn to_f32_column_bytes(values: &[f32]) -> Vec<u8> {
1966 values.iter().flat_map(|v| v.to_le_bytes()).collect()
1967 }
1968
1969 #[test]
1972 fn test_executor_creation() {
1973 let executor = match create_test_executor() {
1974 Some(e) => e,
1975 None => {
1976 eprintln!("Skipping test: no CUDA device available");
1977 return;
1978 }
1979 };
1980
1981 assert!(executor.store().is_empty());
1982 }
1983
1984 #[test]
1985 fn test_predicate_f64_comparisons() {
1986 let schema = Schema::new(vec![("x".to_string(), ScalarType::F64)]);
1987 let values = [1.0f64, 2.0, 3.0, f64::NAN];
1988 let columns = vec![to_f64_column_bytes(&values)];
1989
1990 let gt_two = Expr::Compare {
1991 left: Box::new(Expr::Column(0)),
1992 op: CompareOp::Gt,
1993 right: Box::new(Expr::Const(ConstValue::F64(2.0))),
1994 };
1995
1996 let results: Vec<bool> = (0..values.len())
1997 .map(|row| Executor::evaluate_predicate(>_two, &columns, row, &schema).unwrap())
1998 .collect();
1999 assert_eq!(results, vec![false, false, true, false]);
2000
2001 let eq_nan = Expr::Compare {
2002 left: Box::new(Expr::Column(0)),
2003 op: CompareOp::Eq,
2004 right: Box::new(Expr::Const(ConstValue::F64(f64::NAN))),
2005 };
2006 let results: Vec<bool> = (0..values.len())
2007 .map(|row| Executor::evaluate_predicate(&eq_nan, &columns, row, &schema).unwrap())
2008 .collect();
2009 assert_eq!(results, vec![false, false, false, false]);
2010
2011 let ne_nan = Expr::Compare {
2012 left: Box::new(Expr::Column(0)),
2013 op: CompareOp::Ne,
2014 right: Box::new(Expr::Const(ConstValue::F64(f64::NAN))),
2015 };
2016 let results: Vec<bool> = (0..values.len())
2017 .map(|row| Executor::evaluate_predicate(&ne_nan, &columns, row, &schema).unwrap())
2018 .collect();
2019 assert_eq!(results, vec![true, true, true, true]);
2020 }
2021
2022 #[test]
2023 fn test_predicate_f32_comparisons() {
2024 let schema = Schema::new(vec![("x".to_string(), ScalarType::F32)]);
2025 let values = [1.0f32, 2.0, 3.0, f32::NAN];
2026 let columns = vec![to_f32_column_bytes(&values)];
2027
2028 let le_two = Expr::Compare {
2029 left: Box::new(Expr::Column(0)),
2030 op: CompareOp::Le,
2031 right: Box::new(Expr::Const(ConstValue::F32(2.0))),
2032 };
2033
2034 let results: Vec<bool> = (0..values.len())
2035 .map(|row| Executor::evaluate_predicate(&le_two, &columns, row, &schema).unwrap())
2036 .collect();
2037 assert_eq!(results, vec![true, true, false, false]);
2038 }
2039
2040 #[test]
2041 fn test_predicate_mixed_float_int_comparisons() {
2042 let schema = Schema::new(vec![
2043 ("x".to_string(), ScalarType::F64),
2044 ("y".to_string(), ScalarType::U32),
2045 ]);
2046
2047 let x = [1.5f64, 2.0, 2.5];
2048 let y = [1u32, 2, 3];
2049 let columns = vec![
2050 to_f64_column_bytes(&x),
2051 y.iter().flat_map(|v| v.to_le_bytes()).collect(),
2052 ];
2053
2054 let x_gt_2 = Expr::Compare {
2055 left: Box::new(Expr::Column(0)),
2056 op: CompareOp::Gt,
2057 right: Box::new(Expr::Const(ConstValue::U32(2))),
2058 };
2059 let results: Vec<bool> = (0..x.len())
2060 .map(|row| Executor::evaluate_predicate(&x_gt_2, &columns, row, &schema).unwrap())
2061 .collect();
2062 assert_eq!(results, vec![false, false, true]);
2063
2064 let y_lt_2_5 = Expr::Compare {
2065 left: Box::new(Expr::Column(1)),
2066 op: CompareOp::Lt,
2067 right: Box::new(Expr::Const(ConstValue::F64(2.5))),
2068 };
2069 let results: Vec<bool> = (0..y.len())
2070 .map(|row| Executor::evaluate_predicate(&y_lt_2_5, &columns, row, &schema).unwrap())
2071 .collect();
2072 assert_eq!(results, vec![true, true, false]);
2073 }
2074
2075 #[test]
2076 fn test_register_and_get_relation() {
2077 let mut executor = match create_test_executor() {
2078 Some(e) => e,
2079 None => {
2080 eprintln!("Skipping test: no CUDA device available");
2081 return;
2082 }
2083 };
2084
2085 executor.register_relation(RelId(1), "test_rel");
2087
2088 assert_eq!(executor.get_rel_name(RelId(1)), Some("test_rel"));
2090 assert_eq!(executor.get_rel_name(RelId(2)), None);
2091 }
2092
2093 #[test]
2096 fn test_execute_scan_not_found() {
2097 let mut executor = match create_test_executor() {
2098 Some(e) => e,
2099 None => {
2100 eprintln!("Skipping test: no CUDA device available");
2101 return;
2102 }
2103 };
2104
2105 executor.register_relation(RelId(1), "missing_rel");
2106
2107 let node = RirNode::Scan { rel: RelId(1) };
2108 let result = executor.execute_node(&node);
2109
2110 assert!(result.is_err());
2111 }
2112
2113 #[test]
2114 fn test_execute_scan_success() {
2115 let mut executor = match create_test_executor() {
2116 Some(e) => e,
2117 None => {
2118 eprintln!("Skipping test: no CUDA device available");
2119 return;
2120 }
2121 };
2122
2123 let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2125 executor.store_mut().put("test_rel", buffer);
2126 executor.register_relation(RelId(1), "test_rel");
2127
2128 let node = RirNode::Scan { rel: RelId(1) };
2130 let result = executor.execute_node(&node);
2131
2132 assert!(result.is_ok());
2133 let result = result.unwrap();
2134 assert_eq!(buffer_row_count(&executor, &result), 5);
2135
2136 let values = read_buffer_u32(&executor, &result, 0);
2137 assert_eq!(values, vec![1, 2, 3, 4, 5]);
2138 }
2139
2140 #[test]
2143 fn test_execute_filter_empty_input() {
2144 let executor = match create_test_executor() {
2145 Some(e) => e,
2146 None => {
2147 eprintln!("Skipping test: no CUDA device available");
2148 return;
2149 }
2150 };
2151
2152 let schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
2153 let empty = executor.create_empty_buffer(schema).unwrap();
2154
2155 let predicate = Expr::Const(ConstValue::Bool(true));
2156 let result = executor.execute_filter(&empty, &predicate);
2157
2158 assert!(result.is_ok());
2159 let result = result.unwrap();
2160 assert_eq!(buffer_row_count(&executor, &result), 0);
2161 }
2162
2163 #[test]
2164 fn test_execute_filter_all_match() {
2165 let executor = match create_test_executor() {
2166 Some(e) => e,
2167 None => {
2168 eprintln!("Skipping test: no CUDA device available");
2169 return;
2170 }
2171 };
2172
2173 let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2174 let predicate = Expr::Const(ConstValue::Bool(true));
2175
2176 let result = executor.execute_filter(&buffer, &predicate);
2177 assert!(result.is_ok());
2178
2179 let result = result.unwrap();
2180 assert_eq!(buffer_row_count(&executor, &result), 5);
2181 }
2182
2183 #[test]
2184 fn test_execute_filter_none_match() {
2185 let executor = match create_test_executor() {
2186 Some(e) => e,
2187 None => {
2188 eprintln!("Skipping test: no CUDA device available");
2189 return;
2190 }
2191 };
2192
2193 let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2194 let predicate = Expr::Const(ConstValue::Bool(false));
2195
2196 let result = executor.execute_filter(&buffer, &predicate);
2197 assert!(result.is_ok());
2198 let result = result.unwrap();
2199 assert_eq!(buffer_row_count(&executor, &result), 0);
2200 }
2201
2202 #[test]
2203 fn test_execute_filter_comparison() {
2204 let executor = match create_test_executor() {
2205 Some(e) => e,
2206 None => {
2207 eprintln!("Skipping test: no CUDA device available");
2208 return;
2209 }
2210 };
2211
2212 let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2213
2214 let predicate = Expr::Compare {
2216 left: Box::new(Expr::Column(0)),
2217 op: CompareOp::Gt,
2218 right: Box::new(Expr::Const(ConstValue::U32(3))),
2219 };
2220
2221 let result = executor.execute_filter(&buffer, &predicate);
2222 assert!(result.is_ok());
2223
2224 let result = result.unwrap();
2225 assert_eq!(buffer_row_count(&executor, &result), 2);
2226
2227 let values = read_buffer_u32(&executor, &result, 0);
2228 assert_eq!(values, vec![4, 5]);
2229 }
2230
2231 #[test]
2232 fn test_execute_filter_and() {
2233 let executor = match create_test_executor() {
2234 Some(e) => e,
2235 None => {
2236 eprintln!("Skipping test: no CUDA device available");
2237 return;
2238 }
2239 };
2240
2241 let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2242
2243 let predicate = Expr::And(vec![
2245 Expr::Compare {
2246 left: Box::new(Expr::Column(0)),
2247 op: CompareOp::Ge,
2248 right: Box::new(Expr::Const(ConstValue::U32(2))),
2249 },
2250 Expr::Compare {
2251 left: Box::new(Expr::Column(0)),
2252 op: CompareOp::Le,
2253 right: Box::new(Expr::Const(ConstValue::U32(4))),
2254 },
2255 ]);
2256
2257 let result = executor.execute_filter(&buffer, &predicate);
2258 assert!(result.is_ok());
2259
2260 let result = result.unwrap();
2261 assert_eq!(buffer_row_count(&executor, &result), 3);
2262
2263 let values = read_buffer_u32(&executor, &result, 0);
2264 assert_eq!(values, vec![2, 3, 4]);
2265 }
2266
2267 #[test]
2270 fn test_execute_project_empty_input() {
2271 let executor = match create_test_executor() {
2272 Some(e) => e,
2273 None => {
2274 eprintln!("Skipping test: no CUDA device available");
2275 return;
2276 }
2277 };
2278
2279 let schema = Schema::new(vec![
2280 ("a".to_string(), ScalarType::U32),
2281 ("b".to_string(), ScalarType::U32),
2282 ]);
2283 let empty = executor.create_empty_buffer(schema).unwrap();
2284
2285 let result = executor.execute_project(&empty, &[ProjectExpr::Column(0)]);
2286 assert!(result.is_ok());
2287
2288 let result = result.unwrap();
2289 assert_eq!(buffer_row_count(&executor, &result), 0);
2290 assert_eq!(result.arity(), 1);
2291 }
2292
2293 #[test]
2294 fn test_execute_project_reorder() {
2295 let executor = match create_test_executor() {
2296 Some(e) => e,
2297 None => {
2298 eprintln!("Skipping test: no CUDA device available");
2299 return;
2300 }
2301 };
2302
2303 let schema = Schema::new(vec![
2305 ("a".to_string(), ScalarType::U32),
2306 ("b".to_string(), ScalarType::U32),
2307 ]);
2308
2309 let a_data: Vec<u8> = [1u32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
2310 let b_data: Vec<u8> = [10u32, 20, 30]
2311 .iter()
2312 .flat_map(|v| v.to_le_bytes())
2313 .collect();
2314
2315 let mut col_a = executor
2316 .provider
2317 .memory()
2318 .alloc::<u8>(a_data.len())
2319 .unwrap();
2320 let mut col_b = executor
2321 .provider
2322 .memory()
2323 .alloc::<u8>(b_data.len())
2324 .unwrap();
2325
2326 executor
2327 .provider
2328 .device()
2329 .inner()
2330 .htod_sync_copy_into(&a_data, &mut col_a)
2331 .unwrap();
2332 executor
2333 .provider
2334 .device()
2335 .inner()
2336 .htod_sync_copy_into(&b_data, &mut col_b)
2337 .unwrap();
2338
2339 let d_num_rows = device_row_count(&executor, 3);
2340 let buffer =
2341 CudaBuffer::from_columns(vec![col_a.into(), col_b.into()], 3, d_num_rows, schema);
2342
2343 let result =
2345 executor.execute_project(&buffer, &[ProjectExpr::Column(1), ProjectExpr::Column(0)]);
2346 assert!(result.is_ok());
2347
2348 let result = result.unwrap();
2349 assert_eq!(buffer_row_count(&executor, &result), 3);
2350 assert_eq!(result.arity(), 2);
2351
2352 let col0 = read_buffer_u32(&executor, &result, 0);
2354 assert_eq!(col0, vec![10, 20, 30]);
2355
2356 let col1 = read_buffer_u32(&executor, &result, 1);
2358 assert_eq!(col1, vec![1, 2, 3]);
2359 }
2360
2361 #[test]
2362 fn test_execute_computed_projection_wiring() {
2363 let executor = match create_test_executor() {
2366 Some(e) => e,
2367 None => {
2368 eprintln!("Skipping test: no CUDA device available");
2369 return;
2370 }
2371 };
2372
2373 let schema = Schema::new(vec![
2375 ("a".to_string(), ScalarType::U32),
2376 ("b".to_string(), ScalarType::U32),
2377 ]);
2378
2379 let a_data: Vec<u8> = [10u32, 20, 30]
2380 .iter()
2381 .flat_map(|v| v.to_le_bytes())
2382 .collect();
2383 let b_data: Vec<u8> = [1u32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
2384
2385 let mut col_a = executor
2386 .provider
2387 .memory()
2388 .alloc::<u8>(a_data.len())
2389 .unwrap();
2390 let mut col_b = executor
2391 .provider
2392 .memory()
2393 .alloc::<u8>(b_data.len())
2394 .unwrap();
2395
2396 executor
2397 .provider
2398 .device()
2399 .inner()
2400 .htod_sync_copy_into(&a_data, &mut col_a)
2401 .unwrap();
2402 executor
2403 .provider
2404 .device()
2405 .inner()
2406 .htod_sync_copy_into(&b_data, &mut col_b)
2407 .unwrap();
2408
2409 let d_num_rows = device_row_count(&executor, 3);
2410 let buffer =
2411 CudaBuffer::from_columns(vec![col_a.into(), col_b.into()], 3, d_num_rows, schema);
2412
2413 let add_expr = Expr::Add(Box::new(Expr::Column(0)), Box::new(Expr::Column(1)));
2415 let projections = vec![
2416 ProjectExpr::Column(0), ProjectExpr::Computed(add_expr, ScalarType::U32), ];
2419
2420 let result = executor.execute_project(&buffer, &projections);
2421
2422 match result {
2426 Ok(res) => {
2427 assert_eq!(buffer_row_count(&executor, &res), 3);
2429 assert_eq!(res.arity(), 2);
2430
2431 let col0 = read_buffer_u32(&executor, &res, 0);
2433 assert_eq!(col0, vec![10, 20, 30]);
2434
2435 let col1 = read_buffer_u32(&executor, &res, 1);
2437 assert_eq!(col1, vec![11, 22, 33]);
2438 }
2439 Err(e) => {
2440 let err_msg = format!("{}", e);
2443 assert!(
2444 err_msg.contains("not implemented")
2445 || err_msg.contains("not yet implemented")
2446 || err_msg.contains("not supported")
2447 || err_msg.contains("stub")
2448 || err_msg.contains("Unsupported")
2449 || err_msg.contains("arithmetic kernels"),
2450 "Unexpected error: {}. Expected arithmetic kernel stub error.",
2451 err_msg
2452 );
2453 }
2454 }
2455 }
2456
2457 #[test]
2460 fn test_execute_union_empty_inputs() {
2461 let executor = match create_test_executor() {
2462 Some(e) => e,
2463 None => {
2464 eprintln!("Skipping test: no CUDA device available");
2465 return;
2466 }
2467 };
2468
2469 let result = executor.execute_union(&[]);
2470 assert!(result.is_ok());
2471 let result = result.unwrap();
2472 assert_eq!(buffer_row_count(&executor, &result), 0);
2473 }
2474
2475 #[test]
2476 fn test_execute_union_single_input() {
2477 let executor = match create_test_executor() {
2478 Some(e) => e,
2479 None => {
2480 eprintln!("Skipping test: no CUDA device available");
2481 return;
2482 }
2483 };
2484
2485 let buffer = create_test_buffer(&executor, &[1, 2, 3], "key");
2486
2487 let result = executor.execute_union(&[buffer]);
2488 assert!(result.is_ok());
2489
2490 let result = result.unwrap();
2491 assert_eq!(buffer_row_count(&executor, &result), 3);
2492
2493 let values = read_buffer_u32(&executor, &result, 0);
2494 assert_eq!(values, vec![1, 2, 3]);
2495 }
2496
2497 #[test]
2498 fn test_execute_union_multiple_inputs() {
2499 let executor = match create_test_executor() {
2500 Some(e) => e,
2501 None => {
2502 eprintln!("Skipping test: no CUDA device available");
2503 return;
2504 }
2505 };
2506
2507 let buffer1 = create_test_buffer(&executor, &[1, 2], "key");
2508 let buffer2 = create_test_buffer(&executor, &[3, 4], "key");
2509 let buffer3 = create_test_buffer(&executor, &[5], "key");
2510
2511 let result = executor.execute_union(&[buffer1, buffer2, buffer3]);
2512 assert!(result.is_ok());
2513
2514 let result = result.unwrap();
2515 assert_eq!(buffer_row_count(&executor, &result), 5);
2516 }
2517
2518 #[test]
2521 fn test_execute_distinct_empty() {
2522 let executor = match create_test_executor() {
2523 Some(e) => e,
2524 None => {
2525 eprintln!("Skipping test: no CUDA device available");
2526 return;
2527 }
2528 };
2529
2530 let schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
2531 let empty = executor.create_empty_buffer(schema).unwrap();
2532
2533 let result = executor.execute_distinct(&empty, &[0]);
2534 assert!(result.is_ok());
2535 let result = result.unwrap();
2536 assert_eq!(buffer_row_count(&executor, &result), 0);
2537 }
2538
2539 #[test]
2542 fn test_execute_diff() {
2543 let executor = match create_test_executor() {
2544 Some(e) => e,
2545 None => {
2546 eprintln!("Skipping test: no CUDA device available");
2547 return;
2548 }
2549 };
2550
2551 let left = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2552 let right = create_test_buffer(&executor, &[2, 4], "key");
2553
2554 let result = executor.execute_diff(&left, &right);
2555 assert!(result.is_ok());
2556
2557 let result = result.unwrap();
2558 assert_eq!(buffer_row_count(&executor, &result), 3);
2559
2560 let values = read_buffer_u32(&executor, &result, 0);
2561 assert_eq!(values, vec![1, 3, 5]);
2562 }
2563
2564 #[test]
2567 fn test_execute_fixpoint_base_only() {
2568 let mut executor = match create_test_executor() {
2571 Some(e) => e,
2572 None => {
2573 eprintln!("Skipping test: no CUDA device available");
2574 return;
2575 }
2576 };
2577
2578 let buffer = create_test_buffer(&executor, &[1, 2, 3], "key");
2580 executor.store_mut().put("base_rel", buffer);
2581 executor.register_relation(RelId(1), "base_rel");
2582
2583 let empty_schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
2585 let empty_buffer = executor.create_empty_buffer(empty_schema).unwrap();
2586 executor.store_mut().put("empty_rel", empty_buffer);
2587 executor.register_relation(RelId(4), "empty_rel");
2588
2589 let base = Box::new(RirNode::Scan { rel: RelId(1) });
2592 let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
2593
2594 let node = RirNode::Fixpoint {
2595 scc_id: 0,
2596 base,
2597 recursive,
2598 delta_rel: RelId(2),
2599 full_rel: RelId(3),
2600 };
2601
2602 let result = executor.execute_node(&node);
2603 assert!(result.is_ok());
2604
2605 let result = result.unwrap();
2607 assert_eq!(buffer_row_count(&executor, &result), 3);
2608 let values = read_buffer_u32(&executor, &result, 0);
2609 assert_eq!(values, vec![1, 2, 3]);
2610 }
2611
2612 #[test]
2613 fn test_execute_fixpoint_empty_base() {
2614 let mut executor = match create_test_executor() {
2616 Some(e) => e,
2617 None => {
2618 eprintln!("Skipping test: no CUDA device available");
2619 return;
2620 }
2621 };
2622
2623 let empty_schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
2625 let empty_buffer = executor.create_empty_buffer(empty_schema.clone()).unwrap();
2626 executor.store_mut().put("empty_base", empty_buffer);
2627 executor.register_relation(RelId(1), "empty_base");
2628
2629 let rec_buffer = create_test_buffer(&executor, &[4, 5, 6], "key");
2631 executor.store_mut().put("rec_rel", rec_buffer);
2632 executor.register_relation(RelId(4), "rec_rel");
2633
2634 let base = Box::new(RirNode::Scan { rel: RelId(1) });
2635 let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
2636
2637 let node = RirNode::Fixpoint {
2638 scc_id: 0,
2639 base,
2640 recursive,
2641 delta_rel: RelId(2),
2642 full_rel: RelId(3),
2643 };
2644
2645 let result = executor.execute_node(&node);
2646 assert!(result.is_ok());
2647
2648 let result = result.unwrap();
2650 assert_eq!(buffer_row_count(&executor, &result), 0);
2651 }
2652
2653 #[test]
2654 fn test_execute_fixpoint_one_iteration() {
2655 let mut executor = match create_test_executor() {
2657 Some(e) => e,
2658 None => {
2659 eprintln!("Skipping test: no CUDA device available");
2660 return;
2661 }
2662 };
2663
2664 let base_buffer = create_test_buffer(&executor, &[1, 2], "key");
2666 executor.store_mut().put("base_rel", base_buffer);
2667 executor.register_relation(RelId(1), "base_rel");
2668
2669 let rec_buffer = create_test_buffer(&executor, &[1, 2, 3], "key");
2671 executor.store_mut().put("rec_rel", rec_buffer);
2672 executor.register_relation(RelId(4), "rec_rel");
2673
2674 let base = Box::new(RirNode::Scan { rel: RelId(1) });
2678 let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
2679
2680 let node = RirNode::Fixpoint {
2681 scc_id: 0,
2682 base,
2683 recursive,
2684 delta_rel: RelId(2),
2685 full_rel: RelId(3),
2686 };
2687
2688 let result = executor.execute_node(&node);
2689 assert!(result.is_ok());
2690
2691 let result = result.unwrap();
2692 assert_eq!(buffer_row_count(&executor, &result), 3);
2694 }
2695
2696 #[test]
2697 fn test_execute_fixpoint_multiple_iterations() {
2698 let mut executor = match create_test_executor() {
2701 Some(e) => e,
2702 None => {
2703 eprintln!("Skipping test: no CUDA device available");
2704 return;
2705 }
2706 };
2707
2708 let base_buffer = create_test_buffer(&executor, &[1], "key");
2710 executor.store_mut().put("base_rel", base_buffer);
2711 executor.register_relation(RelId(1), "base_rel");
2712
2713 let rec_buffer = create_test_buffer(&executor, &[1, 2], "key");
2725 executor.store_mut().put("rec_rel", rec_buffer);
2726 executor.register_relation(RelId(4), "rec_rel");
2727
2728 let base = Box::new(RirNode::Scan { rel: RelId(1) });
2729 let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
2730
2731 let node = RirNode::Fixpoint {
2732 scc_id: 0,
2733 base,
2734 recursive,
2735 delta_rel: RelId(2),
2736 full_rel: RelId(3),
2737 };
2738
2739 let result = executor.execute_node(&node);
2740 assert!(result.is_ok());
2741
2742 let result = result.unwrap();
2743 assert_eq!(buffer_row_count(&executor, &result), 2);
2745 }
2746
2747 #[test]
2748 fn test_execute_fixpoint_via_node() {
2749 let mut executor = match create_test_executor() {
2751 Some(e) => e,
2752 None => {
2753 eprintln!("Skipping test: no CUDA device available");
2754 return;
2755 }
2756 };
2757
2758 let buffer = create_test_buffer(&executor, &[1, 2, 3], "key");
2760 executor.store_mut().put("base_rel", buffer);
2761 executor.register_relation(RelId(1), "base_rel");
2762
2763 let empty_schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
2765 let empty_buffer = executor.create_empty_buffer(empty_schema).unwrap();
2766 executor.store_mut().put("empty_rel", empty_buffer);
2767 executor.register_relation(RelId(4), "empty_rel");
2768
2769 let base = Box::new(RirNode::Scan { rel: RelId(1) });
2770 let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
2771
2772 let node = RirNode::Fixpoint {
2773 scc_id: 0,
2774 base,
2775 recursive,
2776 delta_rel: RelId(2),
2777 full_rel: RelId(3),
2778 };
2779
2780 let result = executor.execute_node(&node);
2781 assert!(result.is_ok());
2782
2783 let result = result.unwrap();
2784 assert_eq!(buffer_row_count(&executor, &result), 3);
2785 }
2786
2787 #[test]
2788 fn test_fixpoint_cleanup() {
2789 let mut executor = match create_test_executor() {
2791 Some(e) => e,
2792 None => {
2793 eprintln!("Skipping test: no CUDA device available");
2794 return;
2795 }
2796 };
2797
2798 let buffer = create_test_buffer(&executor, &[1, 2], "key");
2799 executor.store_mut().put("base_rel", buffer);
2800 executor.register_relation(RelId(1), "base_rel");
2801
2802 let empty_schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
2803 let empty_buffer = executor.create_empty_buffer(empty_schema).unwrap();
2804 executor.store_mut().put("empty_rel", empty_buffer);
2805 executor.register_relation(RelId(4), "empty_rel");
2806
2807 executor.register_relation(RelId(2), "__delta_test");
2809 executor.register_relation(RelId(3), "__full_test");
2810
2811 let base = Box::new(RirNode::Scan { rel: RelId(1) });
2812 let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
2813
2814 let node = RirNode::Fixpoint {
2815 scc_id: 0,
2816 base,
2817 recursive,
2818 delta_rel: RelId(2),
2819 full_rel: RelId(3),
2820 };
2821
2822 let result = executor.execute_node(&node);
2823 assert!(result.is_ok());
2824
2825 assert!(!executor.store().contains("__delta_test"));
2827 assert!(!executor.store().contains("__full_test"));
2828 }
2829
2830 #[test]
2833 fn test_execute_plan_empty() {
2834 let mut executor = match create_test_executor() {
2835 Some(e) => e,
2836 None => {
2837 eprintln!("Skipping test: no CUDA device available");
2838 return;
2839 }
2840 };
2841
2842 let plan = ExecutionPlan::new(vec![]);
2843
2844 let result = executor.execute_plan(&plan);
2845 assert!(result.is_ok());
2846 let result = result.unwrap();
2847 assert_eq!(buffer_row_count(&executor, &result), 0);
2848 }
2849
2850 #[test]
2851 fn test_execute_plan_with_stratum() {
2852 let mut executor = match create_test_executor() {
2853 Some(e) => e,
2854 None => {
2855 eprintln!("Skipping test: no CUDA device available");
2856 return;
2857 }
2858 };
2859
2860 let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2862 executor.store_mut().put("input", buffer);
2863 executor.register_relation(RelId(1), "input");
2864
2865 let scc = Scc {
2867 id: 0,
2868 predicates: vec!["output".to_string()],
2869 is_recursive: false,
2870 };
2871
2872 let rule = CompiledRule {
2873 head: "output".to_string(),
2874 body: RirNode::Scan { rel: RelId(1) },
2875 meta: RirMeta::default(),
2876 };
2877
2878 let stratum = Stratum {
2879 id: 0,
2880 sccs: vec![0],
2881 };
2882
2883 let plan = ExecutionPlan {
2884 sccs: vec![scc],
2885 strata: vec![stratum],
2886 rules_by_scc: vec![vec![rule]],
2887 est_memory_peak: 0,
2888 rel_arities: std::collections::HashMap::new(),
2889 };
2890
2891 let result = executor.execute_plan(&plan);
2892 assert!(result.is_ok());
2893
2894 assert!(executor.store().contains("output"));
2896 let output = executor.store().get("output").unwrap();
2897 assert_eq!(buffer_row_count(&executor, output), 5);
2898 }
2899
2900 #[test]
2901 fn test_apply_deltas_and_recompute_updates_dependents() {
2902 let mut executor = match create_test_executor() {
2903 Some(e) => e,
2904 None => {
2905 eprintln!("Skipping test: no CUDA device available");
2906 return;
2907 }
2908 };
2909
2910 let input = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2911 executor.store_mut().put("input", input);
2912 executor.register_relation(RelId(1), "input");
2913
2914 let scc0 = Scc {
2917 id: 0,
2918 predicates: vec!["input".to_string()],
2919 is_recursive: false,
2920 };
2921 let scc1 = Scc {
2922 id: 1,
2923 predicates: vec!["output".to_string()],
2924 is_recursive: false,
2925 };
2926
2927 let input_rule = CompiledRule {
2928 head: "input".to_string(),
2929 body: RirNode::Scan { rel: RelId(1) },
2930 meta: RirMeta::default(),
2931 };
2932
2933 let output_rule = CompiledRule {
2934 head: "output".to_string(),
2935 body: RirNode::Filter {
2936 input: Box::new(RirNode::Scan { rel: RelId(1) }),
2937 predicate: Expr::Compare {
2938 left: Box::new(Expr::Column(0)),
2939 op: CompareOp::Gt,
2940 right: Box::new(Expr::Const(ConstValue::U32(2))),
2941 },
2942 },
2943 meta: RirMeta::default(),
2944 };
2945
2946 let stratum = Stratum {
2947 id: 0,
2948 sccs: vec![0, 1],
2949 };
2950
2951 let plan = ExecutionPlan {
2952 sccs: vec![scc0, scc1],
2953 strata: vec![stratum],
2954 rules_by_scc: vec![vec![input_rule], vec![output_rule]],
2955 est_memory_peak: 0,
2956 rel_arities: std::collections::HashMap::new(),
2957 };
2958
2959 executor.execute_plan(&plan).expect("initial execute_plan");
2960 let initial_out = executor.store().get("output").expect("output missing");
2961 let initial_vals = read_buffer_u32(&executor, initial_out, 0);
2962 assert_eq!(initial_vals, vec![3, 4, 5]);
2963
2964 let delete_buf = create_test_buffer(&executor, &[5], "key");
2965 let insert_buf = create_test_buffer(&executor, &[10], "key");
2966
2967 let mut deltas = HashMap::new();
2968 deltas.insert(
2969 "input".to_string(),
2970 RelationDelta::new(Some(insert_buf), Some(delete_buf)),
2971 );
2972
2973 executor
2974 .apply_deltas_and_recompute(&plan, &deltas)
2975 .expect("apply_deltas_and_recompute");
2976
2977 let out = executor
2978 .store()
2979 .get("output")
2980 .expect("output missing after recompute");
2981 let vals = read_buffer_u32(&executor, out, 0);
2982 assert_eq!(vals, vec![3, 4, 10]);
2983 }
2984
2985 #[test]
2986 fn test_apply_deltas_and_recompute_insert_only_recomputes_anti_join() {
2987 let mut executor = match create_test_executor() {
2988 Some(e) => e,
2989 None => {
2990 eprintln!("Skipping test: no CUDA device available");
2991 return;
2992 }
2993 };
2994
2995 let lhs = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2996 executor.store_mut().put("lhs", lhs);
2997 executor.register_relation(RelId(1), "lhs");
2998
2999 let blocked = create_test_buffer(&executor, &[], "key");
3000 executor.store_mut().put("blocked", blocked);
3001 executor.register_relation(RelId(2), "blocked");
3002
3003 let scc0 = Scc {
3007 id: 0,
3008 predicates: vec!["lhs".to_string()],
3009 is_recursive: false,
3010 };
3011 let scc1 = Scc {
3012 id: 1,
3013 predicates: vec!["blocked".to_string()],
3014 is_recursive: false,
3015 };
3016 let scc2 = Scc {
3017 id: 2,
3018 predicates: vec!["out".to_string()],
3019 is_recursive: false,
3020 };
3021
3022 let lhs_rule = CompiledRule {
3023 head: "lhs".to_string(),
3024 body: RirNode::Scan { rel: RelId(1) },
3025 meta: RirMeta::default(),
3026 };
3027 let blocked_rule = CompiledRule {
3028 head: "blocked".to_string(),
3029 body: RirNode::Scan { rel: RelId(2) },
3030 meta: RirMeta::default(),
3031 };
3032 let out_rule = CompiledRule {
3033 head: "out".to_string(),
3034 body: RirNode::Join {
3035 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3036 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3037 left_keys: vec![0],
3038 right_keys: vec![0],
3039 join_type: JoinType::Anti,
3040 },
3041 meta: RirMeta::default(),
3042 };
3043
3044 let stratum = Stratum {
3045 id: 0,
3046 sccs: vec![0, 1, 2],
3047 };
3048
3049 let plan = ExecutionPlan {
3050 sccs: vec![scc0, scc1, scc2],
3051 strata: vec![stratum],
3052 rules_by_scc: vec![vec![lhs_rule], vec![blocked_rule], vec![out_rule]],
3053 est_memory_peak: 0,
3054 rel_arities: std::collections::HashMap::new(),
3055 };
3056
3057 executor.execute_plan(&plan).expect("initial execute_plan");
3058 let initial = executor.store().get("out").expect("out missing");
3059 let initial_vals = read_buffer_u32(&executor, initial, 0);
3060 assert_eq!(initial_vals, vec![1, 2, 3, 4, 5]);
3061
3062 let insert_buf = create_test_buffer(&executor, &[2, 4], "key");
3064 let mut deltas = HashMap::new();
3065 deltas.insert(
3066 "blocked".to_string(),
3067 RelationDelta::new(Some(insert_buf), None),
3068 );
3069
3070 executor
3071 .apply_deltas_and_recompute(&plan, &deltas)
3072 .expect("apply_deltas_and_recompute");
3073
3074 let out = executor
3075 .store()
3076 .get("out")
3077 .expect("out missing after update");
3078 let vals = read_buffer_u32(&executor, out, 0);
3079 assert_eq!(vals, vec![1, 3, 5]);
3080 }
3081
3082 #[test]
3085 fn test_execute_filter_project_chain() {
3086 let mut executor = match create_test_executor() {
3087 Some(e) => e,
3088 None => {
3089 eprintln!("Skipping test: no CUDA device available");
3090 return;
3091 }
3092 };
3093
3094 let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
3096 executor.store_mut().put("input", buffer);
3097 executor.register_relation(RelId(1), "input");
3098
3099 let scan = RirNode::Scan { rel: RelId(1) };
3101 let filter = RirNode::Filter {
3102 input: Box::new(scan),
3103 predicate: Expr::Compare {
3104 left: Box::new(Expr::Column(0)),
3105 op: CompareOp::Gt,
3106 right: Box::new(Expr::Const(ConstValue::U32(2))),
3107 },
3108 };
3109 let project = RirNode::Project {
3110 input: Box::new(filter),
3111 columns: vec![ProjectExpr::Column(0)],
3112 };
3113
3114 let result = executor.execute_node(&project);
3115 assert!(result.is_ok());
3116
3117 let result = result.unwrap();
3118 assert_eq!(buffer_row_count(&executor, &result), 3);
3119
3120 let values = read_buffer_u32(&executor, &result, 0);
3121 assert_eq!(values, vec![3, 4, 5]);
3122 }
3123
3124 fn duplicate_join_union_plan() -> RirNode {
3127 let join = RirNode::Join {
3128 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3129 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3130 left_keys: vec![0],
3131 right_keys: vec![0],
3132 join_type: JoinType::Inner,
3133 };
3134 RirNode::Union {
3135 inputs: vec![join.clone(), join],
3136 }
3137 }
3138
3139 fn seed_cse_join_fixture(executor: &mut Executor, right: &[u32]) {
3140 executor.register_relation(RelId(1), "left");
3141 executor.register_relation(RelId(2), "right");
3142 let left = create_test_buffer(executor, &[1, 2, 3, 4], "key");
3143 let right = create_test_buffer(executor, right, "key");
3144 executor.put_relation("left", left);
3145 executor.put_relation("right", right);
3146 }
3147
3148 #[test]
3149 fn test_common_subexpression_cache_reuses_duplicate_inner_join_when_enabled() {
3150 let mut executor = match create_test_executor_with_config(
3151 RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3152 ) {
3153 Some(e) => e,
3154 None => {
3155 eprintln!("Skipping test: no CUDA device available");
3156 return;
3157 }
3158 };
3159 seed_cse_join_fixture(&mut executor, &[2, 3, 5]);
3160
3161 let result = executor
3162 .execute_node(&duplicate_join_union_plan())
3163 .expect("duplicate join union executes");
3164
3165 assert_eq!(buffer_row_count(&executor, &result), 2);
3166 let stats = executor.common_subexpression_stats();
3167 assert_eq!(stats.hits, 1);
3168 assert!(stats.misses >= 1);
3169 assert_eq!(stats.unsafe_rejections, 0);
3170 }
3171
3172 #[test]
3173 fn test_common_subexpression_off_on_preserves_output_and_records_reuse_only_when_enabled() {
3174 let mut disabled = match create_test_executor_with_config(
3175 RuntimeConfig::default().with_common_subexpression_elimination(Some(false)),
3176 ) {
3177 Some(e) => e,
3178 None => {
3179 eprintln!("Skipping test: no CUDA device available");
3180 return;
3181 }
3182 };
3183 let mut enabled = match create_test_executor_with_config(
3184 RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3185 ) {
3186 Some(e) => e,
3187 None => {
3188 eprintln!("Skipping test: no CUDA device available");
3189 return;
3190 }
3191 };
3192 seed_cse_join_fixture(&mut disabled, &[2, 3, 5]);
3193 seed_cse_join_fixture(&mut enabled, &[2, 3, 5]);
3194 let plan = duplicate_join_union_plan();
3195
3196 disabled.provider.reset_d2h_transfer_count();
3197 enabled.provider.reset_d2h_transfer_count();
3198 let disabled_result = disabled.execute_node(&plan).expect("disabled CSE output");
3199 let enabled_result = enabled.execute_node(&plan).expect("enabled CSE output");
3200 let disabled_d2h = disabled.provider.d2h_transfer_count();
3201 let enabled_d2h = enabled.provider.d2h_transfer_count();
3202
3203 assert_eq!(
3204 read_buffer_u32(&disabled, &disabled_result, 0),
3205 read_buffer_u32(&enabled, &enabled_result, 0)
3206 );
3207 assert_eq!(enabled_d2h, disabled_d2h);
3208 assert_eq!(disabled.common_subexpression_stats().hits, 0);
3209 assert_eq!(disabled.common_subexpression_stats().misses, 0);
3210 assert_eq!(enabled.common_subexpression_stats().hits, 1);
3211 }
3212
3213 #[test]
3214 fn test_common_subexpression_cache_invalidates_on_relation_generation_change() {
3215 let mut executor = match create_test_executor_with_config(
3216 RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3217 ) {
3218 Some(e) => e,
3219 None => {
3220 eprintln!("Skipping test: no CUDA device available");
3221 return;
3222 }
3223 };
3224 seed_cse_join_fixture(&mut executor, &[2, 3, 5]);
3225 let plan = duplicate_join_union_plan();
3226
3227 executor.execute_node(&plan).expect("first execution");
3228 assert_eq!(executor.common_subexpression_stats().hits, 1);
3229
3230 let changed_right = create_test_buffer(&executor, &[4], "key");
3231 executor.put_relation("right", changed_right);
3232 let result = executor.execute_node(&plan).expect("second execution");
3233
3234 assert_eq!(buffer_row_count(&executor, &result), 1);
3235 let stats = executor.common_subexpression_stats();
3236 assert_eq!(stats.hits, 2);
3237 assert!(stats.misses >= 2);
3238 }
3239
3240 #[test]
3241 fn test_common_subexpression_cache_rejects_unsafe_difference_boundary() {
3242 let mut executor = match create_test_executor_with_config(
3243 RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3244 ) {
3245 Some(e) => e,
3246 None => {
3247 eprintln!("Skipping test: no CUDA device available");
3248 return;
3249 }
3250 };
3251 seed_cse_join_fixture(&mut executor, &[2, 3, 5]);
3252 let diff = RirNode::Diff {
3253 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3254 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3255 };
3256
3257 executor
3258 .execute_node(&RirNode::Union {
3259 inputs: vec![diff.clone(), diff],
3260 })
3261 .expect("unsafe duplicate diff still executes without CSE sharing");
3262
3263 let stats = executor.common_subexpression_stats();
3264 assert_eq!(stats.hits, 0);
3265 assert!(stats.unsafe_rejections >= 1);
3266 assert!(stats
3267 .rejection_reasons
3268 .iter()
3269 .any(|reason| reason == "negation_or_difference_boundary"));
3270 }
3271
3272 #[test]
3273 fn test_common_subexpression_key_rejects_aggregate_and_tensor_boundaries() {
3274 let mut executor = match create_test_executor_with_config(
3275 RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3276 ) {
3277 Some(e) => e,
3278 None => {
3279 eprintln!("Skipping test: no CUDA device available");
3280 return;
3281 }
3282 };
3283 let aggregate = RirNode::GroupBy {
3284 input: Box::new(RirNode::Scan { rel: RelId(1) }),
3285 key_cols: vec![0],
3286 aggs: vec![(0, xlog_core::AggOp::Count)],
3287 };
3288 let tensor = RirNode::TensorMaskedJoin {
3289 mask_name: "W".to_string(),
3290 schema_size: 1,
3291 left_keys: vec![0],
3292 right_keys: vec![0],
3293 rel_index: vec![(RelId(1), "left".to_string())],
3294 head_rel_name: "head".to_string(),
3295 head_rel_id: RelId(3),
3296 max_active_rules: 1,
3297 head_projection: vec![0],
3298 };
3299 let chain = RirNode::ChainJoin {
3300 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3301 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3302 left_key: 0,
3303 right_key: 0,
3304 output_columns: vec![ProjectExpr::Column(0)],
3305 fallback: Box::new(RirNode::Join {
3306 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3307 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3308 left_keys: vec![0],
3309 right_keys: vec![0],
3310 join_type: JoinType::Inner,
3311 }),
3312 };
3313
3314 assert!(executor.common_subexpression_key(&aggregate).is_none());
3315 assert!(executor.common_subexpression_key(&tensor).is_none());
3316 assert!(executor.common_subexpression_key(&chain).is_none());
3317
3318 let reasons = &executor.common_subexpression_stats().rejection_reasons;
3319 assert!(reasons.iter().any(|reason| reason == "aggregate_boundary"));
3320 assert!(reasons
3321 .iter()
3322 .any(|reason| reason == "provenance_or_tensor_boundary"));
3323 assert!(reasons
3324 .iter()
3325 .any(|reason| reason == "specialized_dispatch_boundary"));
3326 }
3327
3328 fn adaptive_scc() -> Scc {
3331 Scc {
3332 id: 0,
3333 predicates: vec!["out".to_string()],
3334 is_recursive: false,
3335 }
3336 }
3337
3338 fn adaptive_stratum() -> Stratum {
3339 Stratum {
3340 id: 0,
3341 sccs: vec![0],
3342 }
3343 }
3344
3345 fn adaptive_rule(body: RirNode) -> CompiledRule {
3346 CompiledRule {
3347 head: "out".to_string(),
3348 body,
3349 meta: RirMeta::default(),
3350 }
3351 }
3352
3353 fn adaptive_plan(body: RirNode) -> ExecutionPlan {
3354 ExecutionPlan {
3355 sccs: vec![adaptive_scc()],
3356 strata: vec![adaptive_stratum()],
3357 rules_by_scc: vec![vec![adaptive_rule(body)]],
3358 est_memory_peak: 0,
3359 rel_arities: std::collections::HashMap::new(),
3360 }
3361 }
3362
3363 fn adaptive_baseline_join_plan() -> ExecutionPlan {
3364 adaptive_plan(RirNode::Project {
3365 input: Box::new(RirNode::Join {
3366 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3367 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3368 left_keys: vec![0],
3369 right_keys: vec![0],
3370 join_type: JoinType::Inner,
3371 }),
3372 columns: vec![ProjectExpr::Column(0)],
3373 })
3374 }
3375
3376 fn adaptive_scan_candidate_plan(rel: RelId) -> ExecutionPlan {
3377 adaptive_plan(RirNode::Scan { rel })
3378 }
3379
3380 fn seed_adaptive_fixture(executor: &mut Executor, right: &[u32]) {
3381 executor.register_relation(RelId(1), "left");
3382 executor.register_relation(RelId(2), "right");
3383 let left = create_test_buffer(executor, &[1, 2, 3, 4, 5, 6, 7, 8], "key");
3384 let right = create_test_buffer(executor, right, "key");
3385 executor.put_relation("left", left);
3386 executor.put_relation("right", right);
3387 }
3388
3389 #[test]
3390 fn test_adaptive_reoptimization_disabled_uses_baseline_and_records_decision() {
3391 let mut executor = match create_test_executor_with_config(
3392 RuntimeConfig::default().with_adaptive_reoptimization(Some(false)),
3393 ) {
3394 Some(e) => e,
3395 None => {
3396 eprintln!("Skipping test: no CUDA device available");
3397 return;
3398 }
3399 };
3400 seed_adaptive_fixture(&mut executor, &[1, 2, 3, 4, 5, 6, 7, 8]);
3401
3402 let baseline = adaptive_baseline_join_plan();
3403 let candidate = adaptive_scan_candidate_plan(RelId(2));
3404 let result = executor
3405 .execute_plan_with_adaptive_candidate(&baseline, &candidate)
3406 .expect("disabled adaptation executes baseline");
3407
3408 assert_eq!(
3409 read_buffer_u32(&executor, &result, 0),
3410 (1..=8).collect::<Vec<_>>()
3411 );
3412 let stats = executor.adaptive_reoptimization_stats();
3413 assert_eq!(stats.disabled, 1);
3414 assert_eq!(stats.adopted, 0);
3415 assert_eq!(stats.rolled_back, 0);
3416 assert_eq!(
3417 stats.last_decision.as_ref().map(|decision| decision.action),
3418 Some(AdaptiveReoptimizationAction::Disabled)
3419 );
3420 }
3421
3422 #[test]
3423 fn test_adaptive_reoptimization_adopts_equivalent_candidate_and_records_telemetry() {
3424 let mut executor = match create_test_executor_with_config(
3425 RuntimeConfig::default().with_adaptive_reoptimization(Some(true)),
3426 ) {
3427 Some(e) => e,
3428 None => {
3429 eprintln!("Skipping test: no CUDA device available");
3430 return;
3431 }
3432 };
3433 seed_adaptive_fixture(&mut executor, &[1, 2, 3, 4, 5, 6, 7, 8]);
3434 executor.provider.reset_host_transfer_stats();
3435
3436 let baseline = adaptive_baseline_join_plan();
3437 let candidate = adaptive_scan_candidate_plan(RelId(1));
3438 let result = executor
3439 .execute_plan_with_adaptive_candidate(&baseline, &candidate)
3440 .expect("equivalent candidate is adopted");
3441
3442 assert_eq!(
3443 read_buffer_u32(&executor, &result, 0),
3444 (1..=8).collect::<Vec<_>>()
3445 );
3446 let stats = executor.adaptive_reoptimization_stats();
3447 assert_eq!(stats.adopted, 1);
3448 assert_eq!(stats.rolled_back, 0);
3449 assert_eq!(stats.last_observations.len(), 1);
3450 assert!(stats.last_observations[0].cardinality_delta_abs > 0);
3451 assert!(stats.last_observations[0].selectivity_delta_abs > 0.0);
3452 assert_eq!(stats.data_plane_dtoh_calls, 0);
3453 }
3454
3455 #[test]
3456 fn test_adaptive_reoptimization_rolls_back_bad_candidate_with_typed_diagnostic() {
3457 let mut executor = match create_test_executor_with_config(
3458 RuntimeConfig::default().with_adaptive_reoptimization(Some(true)),
3459 ) {
3460 Some(e) => e,
3461 None => {
3462 eprintln!("Skipping test: no CUDA device available");
3463 return;
3464 }
3465 };
3466 seed_adaptive_fixture(&mut executor, &[1, 2, 3, 4, 5, 6, 7, 8]);
3467
3468 let baseline = adaptive_baseline_join_plan();
3469 let bad_candidate = adaptive_scan_candidate_plan(RelId(2));
3470 executor.put_relation("right", create_test_buffer(&executor, &[99], "key"));
3471 let result = executor
3472 .execute_plan_with_adaptive_candidate(&baseline, &bad_candidate)
3473 .expect("bad candidate rolls back to baseline output");
3474
3475 assert_eq!(read_buffer_u32(&executor, &result, 0), Vec::<u32>::new());
3476 let out = executor.store().get("out").expect("rollback restored out");
3477 assert_eq!(read_buffer_u32(&executor, out, 0), Vec::<u32>::new());
3478 let stats = executor.adaptive_reoptimization_stats();
3479 assert_eq!(stats.adopted, 0);
3480 assert_eq!(stats.rolled_back, 1);
3481 assert!(stats.diagnostics.iter().any(|diagnostic| {
3482 diagnostic.kind == AdaptiveReoptimizationDiagnosticKind::CandidateOutputMismatch
3483 }));
3484 }
3485
3486 #[test]
3487 fn test_adaptive_reoptimization_decisions_are_deterministic_under_replay() {
3488 let mut executor = match create_test_executor_with_config(
3489 RuntimeConfig::default().with_adaptive_reoptimization(Some(true)),
3490 ) {
3491 Some(e) => e,
3492 None => {
3493 eprintln!("Skipping test: no CUDA device available");
3494 return;
3495 }
3496 };
3497 seed_adaptive_fixture(&mut executor, &[1, 2, 3, 4, 5, 6, 7, 8]);
3498 let baseline = adaptive_baseline_join_plan();
3499 executor
3500 .execute_plan(&baseline)
3501 .expect("baseline execution records telemetry");
3502 let observations = executor
3503 .adaptive_reoptimization_stats()
3504 .last_observations
3505 .clone();
3506
3507 let first = executor.replay_adaptive_reoptimization_decision(&observations);
3508 for _ in 0..100 {
3509 assert_eq!(
3510 executor.replay_adaptive_reoptimization_decision(&observations),
3511 first
3512 );
3513 }
3514 }
3515
3516 fn persistent_index_join_plan() -> ExecutionPlan {
3519 adaptive_baseline_join_plan()
3520 }
3521
3522 fn persistent_index_heavy_join_plan(repetitions: usize) -> ExecutionPlan {
3523 let mut inputs = Vec::with_capacity(repetitions);
3524 for _ in 0..repetitions {
3525 inputs.push(RirNode::Project {
3526 input: Box::new(RirNode::Join {
3527 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3528 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3529 left_keys: vec![0],
3530 right_keys: vec![0],
3531 join_type: JoinType::Semi,
3532 }),
3533 columns: vec![ProjectExpr::Column(0)],
3534 });
3535 }
3536 adaptive_plan(RirNode::Union { inputs })
3537 }
3538
3539 fn seed_persistent_index_fixture(executor: &mut Executor, rows: u32) {
3540 executor.register_relation(RelId(1), "left");
3541 executor.register_relation(RelId(2), "right");
3542 let values: Vec<u32> = (0..rows).collect();
3543 let left = create_test_buffer(executor, &values, "key");
3544 let right = create_test_buffer(executor, &values, "key");
3545 executor.put_relation("left", left);
3546 executor.put_relation("right", right);
3547 }
3548
3549 fn seed_persistent_index_performance_fixture(
3550 executor: &mut Executor,
3551 left_rows: u32,
3552 right_rows: u32,
3553 ) {
3554 executor.register_relation(RelId(1), "left");
3555 executor.register_relation(RelId(2), "right");
3556 let left_values: Vec<u32> = (0..left_rows).collect();
3557 let right_values: Vec<u32> = (0..right_rows).collect();
3558 let left = create_test_buffer(executor, &left_values, "key");
3559 let right = create_test_buffer(executor, &right_values, "key");
3560 executor.put_relation("left", left);
3561 executor.put_relation("right", right);
3562 }
3563
3564 fn warm_persistent_index(executor: &mut Executor, plan: &ExecutionPlan, times: usize) {
3565 for _ in 0..times {
3566 executor.execute_plan(plan).expect("persistent index plan");
3567 }
3568 }
3569
3570 fn median_duration(samples: &mut [Duration]) -> Duration {
3571 samples.sort_unstable();
3572 samples[samples.len() / 2]
3573 }
3574
3575 fn measure_persistent_index_fixture(
3576 mut executor: Executor,
3577 plan: &ExecutionPlan,
3578 warmup: usize,
3579 iterations: usize,
3580 ) -> (
3581 Duration,
3582 u64,
3583 JoinIndexCacheStats,
3584 xlog_cuda::provider::HostTransferStats,
3585 ) {
3586 let mut output_rows = None;
3587 warm_persistent_index(&mut executor, plan, warmup);
3588 executor.provider.reset_host_transfer_stats();
3589
3590 let mut samples = Vec::with_capacity(iterations);
3591 for _ in 0..iterations {
3592 let start = Instant::now();
3593 let output = executor.execute_plan(plan).expect("persistent index plan");
3594 executor
3595 .provider
3596 .device()
3597 .synchronize()
3598 .expect("sync device");
3599 samples.push(start.elapsed());
3600 output_rows = Some(if let Some(buffer) = executor.store().get("out") {
3601 executor
3602 .buffer_row_count(buffer)
3603 .expect("read output row count")
3604 .into()
3605 } else {
3606 output.num_rows()
3607 });
3608 }
3609
3610 (
3611 median_duration(&mut samples),
3612 output_rows.expect("at least one measured execution"),
3613 executor.join_index_cache_stats(),
3614 executor.provider.host_transfer_stats(),
3615 )
3616 }
3617
3618 #[test]
3619 fn test_persistent_hash_index_reuses_across_repeated_session_evaluations() {
3620 let mut executor = match create_test_executor_with_config(
3621 RuntimeConfig::default().with_persistent_hash_indexes(Some(true)),
3622 ) {
3623 Some(e) => e,
3624 None => {
3625 eprintln!("Skipping test: no CUDA device available");
3626 return;
3627 }
3628 };
3629 seed_persistent_index_fixture(&mut executor, 2_500);
3630 let plan = persistent_index_join_plan();
3631 executor.provider.reset_host_transfer_stats();
3632
3633 warm_persistent_index(&mut executor, &plan, 5);
3634
3635 let stats = executor.join_index_cache_stats();
3636 let transfers = executor.provider.host_transfer_stats();
3637 assert_eq!(stats.builds, 1);
3638 assert!(stats.hits >= 1);
3639 assert_eq!(stats.stale_rejections, 0);
3640 assert_eq!(stats.entries, 1);
3641 assert!(stats.total_bytes > 0);
3642 assert_eq!(transfers.dtoh_calls, 0);
3643 assert_eq!(transfers.htod_calls, 0);
3644 }
3645
3646 #[test]
3647 fn test_persistent_hash_index_invalidates_on_relation_generation_change() {
3648 let mut executor = match create_test_executor_with_config(
3649 RuntimeConfig::default().with_persistent_hash_indexes(Some(true)),
3650 ) {
3651 Some(e) => e,
3652 None => {
3653 eprintln!("Skipping test: no CUDA device available");
3654 return;
3655 }
3656 };
3657 seed_persistent_index_fixture(&mut executor, 2_500);
3658 let plan = persistent_index_join_plan();
3659 warm_persistent_index(&mut executor, &plan, 5);
3660 assert_eq!(executor.join_index_cache_stats().entries, 1);
3661
3662 let changed_values: Vec<u32> = (10_000..12_500).collect();
3663 let changed_right = create_test_buffer(&executor, &changed_values, "key");
3664 executor.put_relation("right", changed_right);
3665
3666 let stats = executor.join_index_cache_stats();
3667 assert_eq!(stats.entries, 0);
3668 assert!(stats.invalidations >= 1);
3669 }
3670
3671 #[test]
3672 fn test_persistent_hash_index_background_build_records_requests() {
3673 let mut executor = match create_test_executor_with_config(
3674 RuntimeConfig::default()
3675 .with_persistent_hash_indexes(Some(true))
3676 .with_persistent_hash_index_background_build(Some(true)),
3677 ) {
3678 Some(e) => e,
3679 None => {
3680 eprintln!("Skipping test: no CUDA device available");
3681 return;
3682 }
3683 };
3684 seed_persistent_index_fixture(&mut executor, 2_500);
3685 let plan = persistent_index_join_plan();
3686
3687 warm_persistent_index(&mut executor, &plan, 5);
3688
3689 let stats = executor.join_index_cache_stats();
3690 assert_eq!(stats.background_build_requests, 1);
3691 assert_eq!(stats.background_builds_completed, 1);
3692 assert_eq!(stats.entries, 1);
3693 }
3694
3695 #[test]
3696 fn test_persistent_hash_index_background_build_defers_current_join_reuse() {
3697 let mut executor = match create_test_executor_with_config(
3698 RuntimeConfig::default()
3699 .with_persistent_hash_indexes(Some(true))
3700 .with_persistent_hash_index_background_build(Some(true)),
3701 ) {
3702 Some(e) => e,
3703 None => {
3704 eprintln!("Skipping test: no CUDA device available");
3705 return;
3706 }
3707 };
3708 seed_persistent_index_fixture(&mut executor, 2_500);
3709 let plan = persistent_index_join_plan();
3710
3711 let mut before_build = executor.join_index_cache_stats();
3712 let mut after_build = None;
3713 for _ in 0..5 {
3714 executor
3715 .execute_plan(&plan)
3716 .expect("background-build warm evaluation");
3717 let stats = executor.join_index_cache_stats();
3718 if stats.background_build_requests > before_build.background_build_requests {
3719 after_build = Some(stats);
3720 break;
3721 }
3722 before_build = stats;
3723 }
3724
3725 let after_first = after_build.expect("background build request observed");
3726 assert_eq!(after_first.background_build_requests, 1);
3727 assert_eq!(after_first.background_builds_completed, 1);
3728 assert_eq!(after_first.background_builds_deferred, 1);
3729 assert_eq!(
3730 after_first.hits, before_build.hits,
3731 "background build must not be consumed by the same evaluation that requested it"
3732 );
3733 assert_eq!(after_first.entries, 1);
3734
3735 executor
3736 .execute_plan(&plan)
3737 .expect("second evaluation reuses completed background index");
3738 let after_second = executor.join_index_cache_stats();
3739 assert_eq!(after_second.background_build_requests, 1);
3740 assert_eq!(after_second.background_builds_deferred, 1);
3741 assert!(after_second.hits >= 1);
3742 }
3743
3744 #[test]
3745 fn test_persistent_hash_index_performance_fixture_meets_speedup_target() {
3746 const LEFT_ROWS: u32 = 8;
3747 const RIGHT_ROWS: u32 = 8_000_000;
3748 const JOIN_REPETITIONS: usize = 1;
3749 const WARMUP: usize = 12;
3750 const ITERATIONS: usize = 9;
3751
3752 let mut cached = match create_test_executor_with_config(
3753 RuntimeConfig::default().with_persistent_hash_indexes(Some(true)),
3754 ) {
3755 Some(e) => e,
3756 None => {
3757 eprintln!("Skipping test: no CUDA device available");
3758 return;
3759 }
3760 };
3761 seed_persistent_index_performance_fixture(&mut cached, LEFT_ROWS, RIGHT_ROWS);
3762
3763 let mut uncached = match create_test_executor_with_config(
3764 RuntimeConfig::default().with_persistent_hash_indexes(Some(false)),
3765 ) {
3766 Some(e) => e,
3767 None => {
3768 eprintln!("Skipping test: no CUDA device available");
3769 return;
3770 }
3771 };
3772 seed_persistent_index_performance_fixture(&mut uncached, LEFT_ROWS, RIGHT_ROWS);
3773
3774 let plan = persistent_index_heavy_join_plan(JOIN_REPETITIONS);
3775 let (cached_median, cached_rows, cached_stats, cached_transfers) =
3776 measure_persistent_index_fixture(cached, &plan, WARMUP, ITERATIONS);
3777 let (uncached_median, uncached_rows, uncached_stats, uncached_transfers) =
3778 measure_persistent_index_fixture(uncached, &plan, WARMUP, ITERATIONS);
3779
3780 let speedup_ratio = uncached_median.as_secs_f64() / cached_median.as_secs_f64();
3781 eprintln!(
3782 "persistent_hash_index_perf left_rows={} right_rows={} join_repetitions={} warmup={} iterations={} \
3783 cached_median_sec={:.9} uncached_median_sec={:.9} speedup_ratio={:.3} \
3784 cached_output_rows={} uncached_output_rows={} cached_builds={} cached_hits={} \
3785 uncached_builds={} cached_dtoh_calls={} cached_htod_calls={}",
3786 LEFT_ROWS,
3787 RIGHT_ROWS,
3788 JOIN_REPETITIONS,
3789 WARMUP,
3790 ITERATIONS,
3791 cached_median.as_secs_f64(),
3792 uncached_median.as_secs_f64(),
3793 speedup_ratio,
3794 cached_rows,
3795 uncached_rows,
3796 cached_stats.builds,
3797 cached_stats.hits,
3798 uncached_stats.builds,
3799 cached_transfers.dtoh_calls,
3800 cached_transfers.htod_calls
3801 );
3802
3803 assert_eq!(cached_rows, uncached_rows);
3804 assert_eq!(cached_rows, LEFT_ROWS as u64);
3805 assert_eq!(cached_stats.builds, 1);
3806 assert!(cached_stats.hits >= ITERATIONS as u64);
3807 assert_eq!(uncached_stats.builds, 0);
3808 assert_eq!(cached_transfers.dtoh_calls, 0);
3809 assert_eq!(cached_transfers.htod_calls, 0);
3810 assert_eq!(uncached_transfers.dtoh_calls, 0);
3811 assert_eq!(uncached_transfers.htod_calls, 0);
3812 assert!(
3813 speedup_ratio >= 1.5,
3814 "persistent index speedup {:.3} below 1.5 target",
3815 speedup_ratio
3816 );
3817 }
3818
3819 #[test]
3822 fn test_reset_for_mc_relations_preserves_static_and_clears_dynamic() {
3823 let mut executor = match create_test_executor() {
3824 Some(e) => e,
3825 None => {
3826 eprintln!("Skipping: no CUDA device");
3827 return;
3828 }
3829 };
3830
3831 executor.register_relation(RelId(1), "base_rel");
3832 executor.register_relation(RelId(2), "dyn_rel");
3833
3834 let schema = Schema::new(vec![("x".to_string(), ScalarType::U32)]);
3835 let base = create_test_buffer(&executor, &[1u32], "x");
3836 let dyn_buf = create_test_buffer(&executor, &[9u32], "x");
3837 executor.put_relation("base_rel", base);
3838 executor.put_relation("dyn_rel", dyn_buf);
3839
3840 executor
3841 .reset_for_mc_relations(&["base_rel"], &[("dyn_rel", schema.clone())])
3842 .unwrap();
3843
3844 assert_eq!(
3845 buffer_row_count(&executor, executor.store().get("base_rel").unwrap()),
3846 1
3847 );
3848 assert_eq!(
3849 buffer_row_count(&executor, executor.store().get("dyn_rel").unwrap()),
3850 0
3851 );
3852 }
3853}