1use std::collections::{HashMap, HashSet};
7use std::sync::{Arc, OnceLock};
8
9#[cfg(test)]
10use xlog_core::{f64_total_order_key, 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, WcojFallbackStats};
21use crate::RelationStore;
22
23mod delta;
24mod epistemic_workspace;
25mod expression;
26mod join_cache;
27mod node_dispatch;
28mod recursive;
29mod resident;
30#[cfg(all(test, feature = "resident-graph-tests"))]
31mod resident_graph_tests;
32mod rewrite;
33mod wcoj_cost_model;
34mod wcoj_dispatch;
35#[cfg(feature = "wcoj-phase-timing")]
36pub mod wcoj_phase_timing;
37pub use epistemic_workspace::{
38 EpistemicGpuBatchExecutionResult, EpistemicGpuBatchExecutionTrace,
39 EpistemicGpuCandidateGenerationTrace, EpistemicGpuCandidateValidationTrace,
40 EpistemicGpuConstraintValidationTrace, EpistemicGpuConstraintWorldViewValidationTrace,
41 EpistemicGpuExecutionResult, EpistemicGpuFinalResultMaterializationTrace,
42 EpistemicGpuFinalResultTransferTrace, EpistemicGpuFinalTupleMaterializationTrace,
43 EpistemicGpuKernelTimingTrace, EpistemicGpuMaterializationTrace,
44 EpistemicGpuModelMembershipSource, EpistemicGpuModelMembershipTrace,
45 EpistemicGpuPreparedExecution, EpistemicGpuPropagationTrace, EpistemicGpuProviderIdentity,
46 EpistemicGpuRejectionReason, EpistemicGpuRuntimeCounters, EpistemicGpuRuntimePreflight,
47 EpistemicGpuRuntimeTrace, EpistemicGpuRuntimeWcojCertification,
48 EpistemicGpuTransferBudgetTrace, EpistemicGpuWorkspace, EpistemicGpuWorkspaceCapacities,
49 EpistemicGpuWorkspaceLayout, EpistemicGpuWorkspaceResetTrace,
50 EpistemicGpuWorldViewValidationTrace,
51};
52use join_cache::JoinIndexCache;
53pub use join_cache::JoinIndexCacheStats;
54pub use resident::PreparedResidentGraph;
55
56pub struct RelationDelta {
58 pub insert: Option<CudaBuffer>,
60 pub delete: Option<CudaBuffer>,
62}
63
64impl RelationDelta {
65 pub fn new(insert: Option<CudaBuffer>, delete: Option<CudaBuffer>) -> Self {
67 Self { insert, delete }
68 }
69}
70
71#[derive(Clone, Debug, Default, PartialEq, Eq)]
73pub struct DeltaRecomputeStats {
74 pub changed_relations: usize,
76 pub has_deletes: bool,
78 pub affected_sccs: usize,
80 pub recomputed_sccs: usize,
82 pub incremental_sccs: usize,
84}
85
86#[derive(Clone, Debug, Default, PartialEq, Eq)]
88pub struct CommonSubexpressionStats {
89 pub hits: u64,
91 pub misses: u64,
93 pub unsafe_rejections: u64,
95 pub rejection_reasons: Vec<String>,
97}
98
99#[derive(Clone, Debug, PartialEq)]
101pub struct AdaptiveJoinObservation {
102 pub left_rel: RelId,
104 pub right_rel: RelId,
106 pub estimated_output_rows: u64,
108 pub actual_output_rows: u64,
110 pub cardinality_delta_abs: u64,
112 pub estimated_selectivity: f64,
114 pub actual_selectivity: f64,
116 pub selectivity_delta_abs: f64,
118 pub left_heat: f32,
120 pub right_heat: f32,
122 pub heat_delta_abs: f32,
124 pub misplan_ratio: f64,
126}
127
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub enum AdaptiveReoptimizationAction {
131 Disabled,
133 Skipped,
135 AttemptCandidate,
137 Adopted,
139 RolledBack,
141}
142
143#[derive(Clone, Debug, PartialEq)]
145pub struct AdaptiveReoptimizationDecision {
146 pub action: AdaptiveReoptimizationAction,
148 pub reason: String,
150 pub max_misplan_ratio: f64,
152 pub min_misplan_ratio: f64,
154}
155
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub enum AdaptiveReoptimizationDiagnosticKind {
159 CandidateExecutionFailed,
161 CandidateOutputMismatch,
163 RollbackRestoredBaseline,
165}
166
167#[derive(Clone, Debug, PartialEq, Eq)]
169pub struct AdaptiveReoptimizationDiagnostic {
170 pub kind: AdaptiveReoptimizationDiagnosticKind,
172 pub message: String,
174}
175
176#[derive(Clone, Debug, Default, PartialEq)]
178pub struct AdaptiveReoptimizationStats {
179 pub invocations: u64,
181 pub disabled: u64,
183 pub skipped: u64,
185 pub adopted: u64,
187 pub rolled_back: u64,
189 pub last_decision: Option<AdaptiveReoptimizationDecision>,
191 pub last_observations: Vec<AdaptiveJoinObservation>,
193 pub diagnostics: Vec<AdaptiveReoptimizationDiagnostic>,
195 pub data_plane_dtoh_calls: u64,
197}
198
199#[derive(Clone, Debug, PartialEq, Eq, Hash)]
200enum CommonSubexpressionKey {
201 Scan {
202 rel: RelId,
203 generation: u64,
204 },
205 Filter {
206 input: Box<CommonSubexpressionKey>,
207 predicate: String,
208 },
209 ProjectChain {
210 input: Box<CommonSubexpressionKey>,
211 projections: Vec<Vec<String>>,
212 },
213 Join {
214 left: Box<CommonSubexpressionKey>,
215 right: Box<CommonSubexpressionKey>,
216 left_keys: Vec<usize>,
217 right_keys: Vec<usize>,
218 },
219 Union {
220 inputs: Vec<CommonSubexpressionKey>,
221 },
222 Distinct {
223 input: Box<CommonSubexpressionKey>,
224 key_cols: Vec<usize>,
225 },
226}
227
228pub struct Executor {
249 transaction_identity: Arc<()>,
252 provider: Arc<CudaKernelProvider>,
254 store: RelationStore,
256 rel_names: HashMap<RelId, String>,
258 name_to_rel: HashMap<String, RelId>,
260 stats: StatsManager,
262 join_index_cache: JoinIndexCache,
264 common_subexpression_cache: HashMap<CommonSubexpressionKey, CudaBuffer>,
266 common_subexpression_stats: CommonSubexpressionStats,
268 adaptive_reoptimization_stats: AdaptiveReoptimizationStats,
270 adaptive_join_observations: Vec<AdaptiveJoinObservation>,
272 config: RuntimeConfig,
274 profiler: Profiler,
276 ilp_registry: IlpRegistry,
278 ilp_last_result: Option<IlpTaggedResult>,
280 wcoj_triangle_dispatch_count: u64,
286 pub(super) wcoj_4cycle_dispatch_count: u64,
289 pub(super) chain_dispatch_count: u64,
292 pub(super) chain_fallback_scan_equivalents: u64,
296 pub(super) chain_fallback_filter_equivalents: u64,
300 pub(super) wcoj_clique5_dispatch_count: u64,
304 pub(super) wcoj_clique6_dispatch_count: u64,
307 pub(super) wcoj_clique7_dispatch_count: u64,
310 pub(super) wcoj_clique8_dispatch_count: u64,
313 pub(super) kclique_histogram_refresh_count: u64,
316 pub(super) kclique_histogram_refresh_nanos: u128,
319 pub(super) nested_loop_dispatch_count: u64,
328 pub(super) wcoj_error_decline_count: u64,
335 pub(super) wcoj_fallback: WcojFallbackStats,
337 pub(super) wcoj_groupby_fusion_dispatch_count: u64,
341 pub(super) free_join_dispatch_count: u64,
345 pub(super) factorized_delta_dispatch_count: u64,
349 wcoj_dispatch_stream: OnceLock<xlog_cuda::device_runtime::StreamId>,
364 resident_graph_stream: OnceLock<xlog_cuda::device_runtime::StreamId>,
368 #[cfg(feature = "wcoj-phase-timing")]
375 pub(super) last_wcoj_phase_timing:
376 std::sync::Mutex<Option<wcoj_phase_timing::WcojDispatchPhaseTiming>>,
377 #[cfg(feature = "recursive-stats-trace")]
386 pub(super) last_recursive_stats_trace: RecursiveStatsTrace,
387}
388
389#[cfg(feature = "recursive-stats-trace")]
398#[derive(Debug, Default, Clone)]
399pub struct RecursiveStatsTrace {
400 pub entries: Vec<RecursiveStatsTraceEntry>,
402}
403
404#[cfg(feature = "recursive-stats-trace")]
412#[derive(Debug, Clone)]
413pub struct RecursiveStatsTraceEntry {
414 pub iteration: usize,
416 pub pred: String,
418 pub full_rel: RelId,
420 pub delta_rel: RelId,
422 pub full_rows: u64,
424 pub delta_rows: u64,
426 pub phase: RecursiveStatsPhase,
428 pub binary_est_for_variant: Option<u64>,
434}
435
436#[cfg(feature = "recursive-stats-trace")]
437#[derive(Debug, Clone, Copy, PartialEq, Eq)]
438pub enum RecursiveStatsPhase {
440 Seed,
443 Phase2Delta,
447 Phase4Full,
451}
452
453impl Executor {
454 pub fn new(provider: Arc<CudaKernelProvider>) -> Self {
459 Self::new_with_config(provider, RuntimeConfig::default())
460 }
461
462 pub fn new_with_config(provider: Arc<CudaKernelProvider>, config: RuntimeConfig) -> Self {
464 const DEFAULT_JOIN_INDEX_CACHE_BYTES: u64 = 256 * 1024 * 1024;
465 let max_index_cache_bytes =
466 (provider.memory().budget().device_bytes / 4).min(DEFAULT_JOIN_INDEX_CACHE_BYTES);
467 Self {
468 transaction_identity: Arc::new(()),
469 provider: provider.clone(),
470 store: RelationStore::new(provider.clone()),
471 rel_names: HashMap::new(),
472 name_to_rel: HashMap::new(),
473 stats: StatsManager::new(),
474 join_index_cache: JoinIndexCache::new(max_index_cache_bytes),
475 common_subexpression_cache: HashMap::new(),
476 common_subexpression_stats: CommonSubexpressionStats::default(),
477 adaptive_reoptimization_stats: AdaptiveReoptimizationStats::default(),
478 adaptive_join_observations: Vec::new(),
479 config,
480 profiler: Profiler::default(),
481 ilp_registry: IlpRegistry::new(),
482 ilp_last_result: None,
483 wcoj_triangle_dispatch_count: 0,
484 wcoj_4cycle_dispatch_count: 0,
485 chain_dispatch_count: 0,
486 chain_fallback_scan_equivalents: 0,
487 chain_fallback_filter_equivalents: 0,
488 wcoj_clique5_dispatch_count: 0,
489 wcoj_clique6_dispatch_count: 0,
490 wcoj_clique7_dispatch_count: 0,
491 wcoj_clique8_dispatch_count: 0,
492 kclique_histogram_refresh_count: 0,
493 kclique_histogram_refresh_nanos: 0,
494 nested_loop_dispatch_count: 0,
495 wcoj_error_decline_count: 0,
496 wcoj_fallback: WcojFallbackStats::default(),
497 wcoj_groupby_fusion_dispatch_count: 0,
498 free_join_dispatch_count: 0,
499 factorized_delta_dispatch_count: 0,
500 wcoj_dispatch_stream: OnceLock::new(),
501 resident_graph_stream: OnceLock::new(),
502 #[cfg(feature = "wcoj-phase-timing")]
503 last_wcoj_phase_timing: std::sync::Mutex::new(None),
504 #[cfg(feature = "recursive-stats-trace")]
505 last_recursive_stats_trace: RecursiveStatsTrace::default(),
506 }
507 }
508
509 #[cfg(feature = "recursive-stats-trace")]
513 pub fn last_recursive_stats_trace(&self) -> &RecursiveStatsTrace {
514 &self.last_recursive_stats_trace
515 }
516
517 #[cfg(feature = "wcoj-phase-timing")]
526 pub fn take_wcoj_phase_timing(&self) -> Option<wcoj_phase_timing::WcojDispatchPhaseTiming> {
527 self.last_wcoj_phase_timing
528 .lock()
529 .ok()
530 .and_then(|mut g| g.take())
531 }
532
533 pub fn set_profiling(&mut self, enabled: bool) {
537 self.profiler = Profiler::new(enabled);
538 if enabled {
539 self.profiler
540 .set_memory_budget(self.provider.memory().budget().device_bytes);
541 }
542 }
543
544 pub fn is_profiling(&self) -> bool {
546 self.profiler.is_enabled()
547 }
548
549 pub fn execution_stats(&self, total_output_rows: u64) -> ExecutionStats {
555 let mut stats = self.profiler.execution_stats(total_output_rows);
556 stats.peak_memory_bytes = self.provider.memory().peak_bytes();
557 stats.wcoj_triangle_dispatch_count = self.wcoj_triangle_dispatch_count();
562 stats.wcoj_4cycle_dispatch_count = self.wcoj_4cycle_dispatch_count();
563 stats.wcoj_groupby_fusion_dispatch_count = self.wcoj_groupby_fusion_dispatch_count();
564 stats.free_join_dispatch_count = self.free_join_dispatch_count();
565 stats.factorized_delta_dispatch_count = self.factorized_delta_dispatch_count();
566 stats.wcoj_error_decline_count = self.wcoj_error_decline_count();
567 stats.wcoj_fallback = self.wcoj_fallback_stats();
568 stats.chain_fallback_scan_equivalents = self.chain_fallback_scan_equivalents;
569 stats.chain_fallback_filter_equivalents = self.chain_fallback_filter_equivalents;
570 stats
571 }
572
573 pub fn store(&self) -> &RelationStore {
575 &self.store
576 }
577
578 pub fn store_mut(&mut self) -> &mut RelationStore {
580 &mut self.store
581 }
582
583 pub fn ilp_registry_mut(&mut self) -> &mut IlpRegistry {
585 &mut self.ilp_registry
586 }
587
588 pub fn ilp_registry(&self) -> &IlpRegistry {
590 &self.ilp_registry
591 }
592
593 pub fn ilp_last_result(&self) -> Option<&IlpTaggedResult> {
595 self.ilp_last_result.as_ref()
596 }
597
598 pub fn put_relation(&mut self, name: &str, buffer: CudaBuffer) {
600 self.store_put(name, buffer);
601 }
602
603 pub fn stats(&self) -> &StatsManager {
605 &self.stats
606 }
607
608 pub fn join_index_cache_stats(&self) -> JoinIndexCacheStats {
610 self.join_index_cache.stats()
611 }
612
613 pub fn reset_for_mc(&mut self) {
617 self.store.clear();
618 self.join_index_cache.clear();
619 self.common_subexpression_cache.clear();
620 self.adaptive_join_observations.clear();
621 }
622
623 pub fn reset_for_mc_relations(
636 &mut self,
637 preserve: &[&str],
638 clear_to_empty: &[(&str, Schema)],
639 ) -> Result<()> {
640 let preserve_set: HashSet<&str> = preserve.iter().copied().collect();
641 let existing_names: Vec<String> = self.store.names().map(|s| s.to_string()).collect();
642
643 for name in &existing_names {
644 if !preserve_set.contains(name.as_str()) {
645 self.store.remove(name);
646 }
647 }
648
649 for (name, schema) in clear_to_empty {
650 let empty = self.provider.create_empty_buffer(schema.clone())?;
651 self.store.put(name, empty);
652 }
653
654 self.join_index_cache.clear();
655 self.common_subexpression_cache.clear();
656 self.adaptive_join_observations.clear();
657 Ok(())
658 }
659
660 pub fn reset_for_ilp(&mut self) {
667 self.ilp_registry.clear();
668 self.ilp_last_result = None;
669 self.store.clear();
670 self.join_index_cache.clear();
671 self.common_subexpression_cache.clear();
672 self.adaptive_join_observations.clear();
673 self.stats = StatsManager::new();
674 self.profiler = Profiler::default();
675 }
676
677 pub fn stats_mut(&mut self) -> &mut StatsManager {
679 &mut self.stats
680 }
681
682 pub fn stats_snapshot(&self) -> StatsSnapshot {
686 let mut snapshot = self.stats.snapshot();
687 snapshot.rel_names = self
688 .rel_names
689 .iter()
690 .map(|(id, name)| (*id, name.clone()))
691 .collect();
692 snapshot
693 }
694
695 pub fn common_subexpression_stats(&self) -> &CommonSubexpressionStats {
697 &self.common_subexpression_stats
698 }
699
700 pub fn adaptive_reoptimization_stats(&self) -> &AdaptiveReoptimizationStats {
702 &self.adaptive_reoptimization_stats
703 }
704
705 pub fn replay_adaptive_reoptimization_decision(
707 &self,
708 observations: &[AdaptiveJoinObservation],
709 ) -> Result<AdaptiveReoptimizationDecision> {
710 self.adaptive_reoptimization_decision(observations)
711 }
712
713 fn common_subexpression_enabled(&self) -> Result<bool> {
714 self.config.resolved_common_subexpression_elimination()
715 }
716
717 fn adaptive_reoptimization_enabled(&self) -> Result<bool> {
718 self.config.resolved_adaptive_reoptimization()
719 }
720
721 fn adaptive_reoptimization_decision(
722 &self,
723 observations: &[AdaptiveJoinObservation],
724 ) -> Result<AdaptiveReoptimizationDecision> {
725 let min_misplan_ratio = self
726 .config
727 .resolved_adaptive_reoptimization_min_misplan_ratio();
728 let max_misplan_ratio = observations
729 .iter()
730 .map(|observation| observation.misplan_ratio)
731 .fold(1.0_f64, f64::max);
732
733 if !self.adaptive_reoptimization_enabled()? {
734 return Ok(AdaptiveReoptimizationDecision {
735 action: AdaptiveReoptimizationAction::Disabled,
736 reason: "adaptive_reoptimization_disabled".to_string(),
737 max_misplan_ratio,
738 min_misplan_ratio,
739 });
740 }
741
742 if observations.is_empty() {
743 return Ok(AdaptiveReoptimizationDecision {
744 action: AdaptiveReoptimizationAction::Skipped,
745 reason: "no_join_telemetry".to_string(),
746 max_misplan_ratio,
747 min_misplan_ratio,
748 });
749 }
750
751 Ok(if max_misplan_ratio >= min_misplan_ratio {
752 AdaptiveReoptimizationDecision {
753 action: AdaptiveReoptimizationAction::AttemptCandidate,
754 reason: "misplan_threshold_crossed".to_string(),
755 max_misplan_ratio,
756 min_misplan_ratio,
757 }
758 } else {
759 AdaptiveReoptimizationDecision {
760 action: AdaptiveReoptimizationAction::Skipped,
761 reason: "misplan_threshold_not_crossed".to_string(),
762 max_misplan_ratio,
763 min_misplan_ratio,
764 }
765 })
766 }
767
768 fn record_adaptive_join_observation(
769 &mut self,
770 left_rel: RelId,
771 right_rel: RelId,
772 left_keys: &[usize],
773 right_keys: &[usize],
774 input_rows: u64,
775 actual_output_rows: u64,
776 ) {
777 let estimated_output_rows = self
778 .stats
779 .estimate_join_cardinality(left_rel, right_rel, left_keys, right_keys);
780 let estimated_selectivity = if input_rows > 0 {
781 estimated_output_rows as f64 / input_rows as f64
782 } else {
783 0.0
784 };
785 let actual_selectivity = if input_rows > 0 {
786 actual_output_rows as f64 / input_rows as f64
787 } else {
788 0.0
789 };
790 let cardinality_delta_abs = estimated_output_rows.abs_diff(actual_output_rows);
791 let selectivity_delta_abs = (estimated_selectivity - actual_selectivity).abs();
792 let left_heat = self
793 .stats
794 .get_relation_stats(left_rel)
795 .map(|stats| stats.heat)
796 .unwrap_or(0.0);
797 let right_heat = self
798 .stats
799 .get_relation_stats(right_rel)
800 .map(|stats| stats.heat)
801 .unwrap_or(0.0);
802 let heat_delta_abs = (left_heat - right_heat).abs();
803 let smaller = estimated_output_rows.min(actual_output_rows);
804 let larger = estimated_output_rows.max(actual_output_rows);
805 let misplan_ratio = if smaller == 0 {
806 if larger == 0 {
807 1.0
808 } else {
809 f64::INFINITY
810 }
811 } else {
812 (larger as f64 / smaller as f64).max(1.0)
813 };
814
815 self.adaptive_join_observations
816 .push(AdaptiveJoinObservation {
817 left_rel,
818 right_rel,
819 estimated_output_rows,
820 actual_output_rows,
821 cardinality_delta_abs,
822 estimated_selectivity,
823 actual_selectivity,
824 selectivity_delta_abs,
825 left_heat,
826 right_heat,
827 heat_delta_abs,
828 misplan_ratio,
829 });
830 }
831
832 fn plan_head_names(plan: &ExecutionPlan) -> Vec<String> {
833 let mut names = Vec::new();
834 for stratum in &plan.strata {
835 for scc_id in &stratum.sccs {
836 if let Some(rules) = plan.rules_by_scc.get(*scc_id as usize) {
837 for rule in rules {
838 if !names.iter().any(|name| name == &rule.head) {
839 names.push(rule.head.clone());
840 }
841 }
842 }
843 }
844 }
845
846 if names.is_empty() {
847 for rules in &plan.rules_by_scc {
848 for rule in rules {
849 if !names.iter().any(|name| name == &rule.head) {
850 names.push(rule.head.clone());
851 }
852 }
853 }
854 }
855
856 names
857 }
858
859 fn clone_store_snapshot(&self) -> Result<HashMap<String, CudaBuffer>> {
860 let names: Vec<String> = self.store.names().map(|name| name.to_string()).collect();
861 let mut snapshot = HashMap::with_capacity(names.len());
862 for name in names {
863 if let Some(buffer) = self.store.get(&name) {
864 snapshot.insert(name, self.clone_buffer(buffer)?);
865 }
866 }
867 Ok(snapshot)
868 }
869
870 fn restore_store_snapshot(&mut self, snapshot: HashMap<String, CudaBuffer>) {
871 let snapshot_names: HashSet<String> = snapshot.keys().cloned().collect();
872 let existing_names: Vec<String> = self.store.names().map(|name| name.to_string()).collect();
873 for name in existing_names {
874 if !snapshot_names.contains(&name) {
875 self.store.remove(&name);
876 }
877 }
878 for (name, buffer) in snapshot {
879 self.store.put(&name, buffer);
880 }
881 }
882
883 fn restore_stats_snapshot(&mut self, snapshot: &StatsSnapshot) {
884 self.stats.clear();
885 self.stats.merge_snapshot(snapshot);
886 }
887
888 fn clone_final_plan_output(&self, plan: &ExecutionPlan) -> Result<CudaBuffer> {
889 let head_names = Self::plan_head_names(plan);
890 if let Some(name) = head_names.last() {
891 let output = self.store.get(name).ok_or_else(|| {
892 XlogError::Execution(format!("adaptive reoptimization output missing: {name}"))
893 })?;
894 return self.clone_buffer(output);
895 }
896
897 self.provider.create_empty_buffer(Schema::new(vec![]))
898 }
899
900 fn plan_outputs_match(
901 &self,
902 head_names: &[String],
903 baseline_snapshot: &HashMap<String, CudaBuffer>,
904 ) -> Result<bool> {
905 for name in head_names {
906 let Some(baseline) = baseline_snapshot.get(name) else {
907 return Ok(false);
908 };
909 let Some(candidate) = self.store.get(name) else {
910 return Ok(false);
911 };
912 if !self.buffers_gpu_set_equivalent(baseline, candidate)? {
913 return Ok(false);
914 }
915 }
916 Ok(true)
917 }
918
919 fn buffers_gpu_set_equivalent(&self, left: &CudaBuffer, right: &CudaBuffer) -> Result<bool> {
920 if left.schema() != right.schema() {
921 return Ok(false);
922 }
923 let left_rows = self.provider.device_row_count(left)?;
924 let right_rows = self.provider.device_row_count(right)?;
925 if left_rows != right_rows {
926 return Ok(false);
927 }
928
929 let left_minus_right = self.provider.diff_full_row(left, right)?;
930 if self.provider.device_row_count(&left_minus_right)? != 0 {
931 return Ok(false);
932 }
933 let right_minus_left = self.provider.diff_full_row(right, left)?;
934 Ok(self.provider.device_row_count(&right_minus_left)? == 0)
935 }
936
937 fn record_adaptive_dtoh_delta(&mut self, before_dtoh_calls: u64) {
938 let after_dtoh_calls = self.provider.host_transfer_stats().dtoh_calls;
939 self.adaptive_reoptimization_stats.data_plane_dtoh_calls =
940 after_dtoh_calls.saturating_sub(before_dtoh_calls);
941 }
942
943 fn is_common_subexpression_cacheable(node: &RirNode) -> bool {
944 !matches!(node, RirNode::Unit | RirNode::Scan { .. })
945 }
946
947 fn record_common_subexpression_rejection(&mut self, reason: &'static str) {
948 self.common_subexpression_stats.unsafe_rejections = self
949 .common_subexpression_stats
950 .unsafe_rejections
951 .saturating_add(1);
952 if !self
953 .common_subexpression_stats
954 .rejection_reasons
955 .iter()
956 .any(|seen| seen == reason)
957 {
958 self.common_subexpression_stats
959 .rejection_reasons
960 .push(reason.to_string());
961 }
962 }
963
964 fn common_subexpression_key(&mut self, node: &RirNode) -> Option<CommonSubexpressionKey> {
965 match node {
966 RirNode::Unit => None,
967 RirNode::Scan { rel } => {
968 let generation = self
969 .get_rel_name(*rel)
970 .and_then(|name| self.store.version(name))
971 .unwrap_or(0);
972 Some(CommonSubexpressionKey::Scan {
973 rel: *rel,
974 generation,
975 })
976 }
977 RirNode::Filter { input, predicate } => {
978 let input = self.common_subexpression_key(input)?;
979 Some(CommonSubexpressionKey::Filter {
980 input: Box::new(input),
981 predicate: Self::expr_cse_key(predicate),
982 })
983 }
984 RirNode::Project { .. } => self.common_subexpression_project_chain_key(node),
985 RirNode::Join {
986 left,
987 right,
988 left_keys,
989 right_keys,
990 join_type,
991 } => {
992 if *join_type != JoinType::Inner {
993 self.record_common_subexpression_rejection("negation_or_outer_join_boundary");
994 return None;
995 }
996 let left = self.common_subexpression_key(left)?;
997 let right = self.common_subexpression_key(right)?;
998 Some(CommonSubexpressionKey::Join {
999 left: Box::new(left),
1000 right: Box::new(right),
1001 left_keys: left_keys.clone(),
1002 right_keys: right_keys.clone(),
1003 })
1004 }
1005 RirNode::Union { inputs } => {
1006 let mut input_keys = Vec::with_capacity(inputs.len());
1007 for input in inputs {
1008 input_keys.push(self.common_subexpression_key(input)?);
1009 }
1010 Some(CommonSubexpressionKey::Union { inputs: input_keys })
1011 }
1012 RirNode::Distinct { input, key_cols } => {
1013 let input = self.common_subexpression_key(input)?;
1014 Some(CommonSubexpressionKey::Distinct {
1015 input: Box::new(input),
1016 key_cols: key_cols.clone(),
1017 })
1018 }
1019 RirNode::Diff { .. } => {
1020 self.record_common_subexpression_rejection("negation_or_difference_boundary");
1021 None
1022 }
1023 RirNode::GroupBy { .. } => {
1024 self.record_common_subexpression_rejection("aggregate_boundary");
1025 None
1026 }
1027 RirNode::Fixpoint { .. } => {
1028 self.record_common_subexpression_rejection("recursive_or_mutable_boundary");
1029 None
1030 }
1031 RirNode::TensorMaskedJoin { .. } => {
1032 self.record_common_subexpression_rejection("provenance_or_tensor_boundary");
1033 None
1034 }
1035 RirNode::MultiWayJoin { .. } | RirNode::ChainJoin { .. } => {
1036 self.record_common_subexpression_rejection("specialized_dispatch_boundary");
1037 None
1038 }
1039 }
1040 }
1041
1042 fn common_subexpression_project_chain_key(
1043 &mut self,
1044 node: &RirNode,
1045 ) -> Option<CommonSubexpressionKey> {
1046 let mut project_columns = Vec::new();
1050 let mut base = node;
1051 while let RirNode::Project { input, columns } = base {
1052 project_columns.push(columns);
1053 base = input;
1054 }
1055
1056 let input = Box::new(self.common_subexpression_key(base)?);
1057 let projections = project_columns
1058 .into_iter()
1059 .rev()
1060 .map(|columns| columns.iter().map(Self::project_expr_cse_key).collect())
1061 .collect();
1062
1063 Some(CommonSubexpressionKey::ProjectChain { input, projections })
1064 }
1065
1066 fn expr_cse_key(expr: &Expr) -> String {
1067 enum KeyTask<'a> {
1068 Expression(&'a Expr),
1069 FinishUnary(&'static str),
1070 FinishBinary(&'static str),
1071 FinishComparison(xlog_ir::CompareOp),
1072 FinishList {
1073 prefix: &'static str,
1074 item_count: usize,
1075 },
1076 FinishCast(xlog_core::ScalarType),
1077 FinishConditional,
1078 }
1079
1080 fn schedule_binary<'a>(
1081 tasks: &mut Vec<KeyTask<'a>>,
1082 left: &'a Expr,
1083 right: &'a Expr,
1084 prefix: &'static str,
1085 ) {
1086 tasks.push(KeyTask::FinishBinary(prefix));
1087 tasks.push(KeyTask::Expression(right));
1088 tasks.push(KeyTask::Expression(left));
1089 }
1090
1091 let mut tasks = vec![KeyTask::Expression(expr)];
1092 let mut values = Vec::new();
1093 while let Some(task) = tasks.pop() {
1094 match task {
1095 KeyTask::Expression(expression) => match expression {
1096 Expr::Column(index) => values.push(format!("col:{index}")),
1097 Expr::Const(value) => {
1098 values.push(format!("const:{}", Self::const_cse_key(value)));
1099 }
1100 Expr::Compare { left, op, right } => {
1101 tasks.push(KeyTask::FinishComparison(*op));
1102 tasks.push(KeyTask::Expression(right));
1103 tasks.push(KeyTask::Expression(left));
1104 }
1105 Expr::And(items) => {
1106 tasks.push(KeyTask::FinishList {
1107 prefix: "and",
1108 item_count: items.len(),
1109 });
1110 for item in items.iter().rev() {
1111 tasks.push(KeyTask::Expression(item));
1112 }
1113 }
1114 Expr::Or(items) => {
1115 tasks.push(KeyTask::FinishList {
1116 prefix: "or",
1117 item_count: items.len(),
1118 });
1119 for item in items.iter().rev() {
1120 tasks.push(KeyTask::Expression(item));
1121 }
1122 }
1123 Expr::Not(inner) => {
1124 tasks.push(KeyTask::FinishUnary("not"));
1125 tasks.push(KeyTask::Expression(inner));
1126 }
1127 Expr::Add(left, right) => schedule_binary(&mut tasks, left, right, "add"),
1128 Expr::Sub(left, right) => schedule_binary(&mut tasks, left, right, "sub"),
1129 Expr::Mul(left, right) => schedule_binary(&mut tasks, left, right, "mul"),
1130 Expr::Div(left, right) => schedule_binary(&mut tasks, left, right, "div"),
1131 Expr::Mod(left, right) => schedule_binary(&mut tasks, left, right, "mod"),
1132 Expr::Abs(inner) => {
1133 tasks.push(KeyTask::FinishUnary("abs"));
1134 tasks.push(KeyTask::Expression(inner));
1135 }
1136 Expr::Min(left, right) => schedule_binary(&mut tasks, left, right, "min"),
1137 Expr::Max(left, right) => schedule_binary(&mut tasks, left, right, "max"),
1138 Expr::Pow(left, right) => schedule_binary(&mut tasks, left, right, "pow"),
1139 Expr::Cast(inner, target) => {
1140 tasks.push(KeyTask::FinishCast(*target));
1141 tasks.push(KeyTask::Expression(inner));
1142 }
1143 Expr::Conditional {
1144 condition,
1145 then_expr,
1146 else_expr,
1147 } => {
1148 tasks.push(KeyTask::FinishConditional);
1149 tasks.push(KeyTask::Expression(else_expr));
1150 tasks.push(KeyTask::Expression(then_expr));
1151 tasks.push(KeyTask::Expression(condition));
1152 }
1153 },
1154 KeyTask::FinishUnary(prefix) => {
1155 let value = values.pop().expect("unary CSE operand is serialized");
1156 values.push(format!("{prefix}:{value}"));
1157 }
1158 KeyTask::FinishBinary(prefix) => {
1159 let right = values.pop().expect("right CSE operand is serialized");
1160 let left = values.pop().expect("left CSE operand is serialized");
1161 values.push(format!("{prefix}:{left}:{right}"));
1162 }
1163 KeyTask::FinishComparison(op) => {
1164 let right = values.pop().expect("right comparison is serialized");
1165 let left = values.pop().expect("left comparison is serialized");
1166 values.push(format!(
1167 "cmp:{left}:{}:{right}",
1168 Self::compare_op_cse_key(op)
1169 ));
1170 }
1171 KeyTask::FinishList { prefix, item_count } => {
1172 let start = values
1173 .len()
1174 .checked_sub(item_count)
1175 .expect("CSE list items are serialized");
1176 let items = values.split_off(start);
1177 values.push(format!("{prefix}:[{}]", items.join(",")));
1178 }
1179 KeyTask::FinishCast(target) => {
1180 let value = values.pop().expect("cast CSE operand is serialized");
1181 values.push(format!("cast:{target:?}:{value}"));
1182 }
1183 KeyTask::FinishConditional => {
1184 let else_value = values.pop().expect("else CSE branch is serialized");
1185 let then_value = values.pop().expect("then CSE branch is serialized");
1186 let condition = values.pop().expect("CSE condition is serialized");
1187 values.push(format!("if:{condition}:{then_value}:{else_value}"));
1188 }
1189 }
1190 }
1191
1192 let key = values.pop().expect("expression produces one CSE key");
1193 debug_assert!(values.is_empty());
1194 key
1195 }
1196
1197 fn project_expr_cse_key(expr: &ProjectExpr) -> String {
1198 match expr {
1199 ProjectExpr::Column(idx) => format!("col:{idx}"),
1200 ProjectExpr::Computed(expr, ty) => {
1201 format!("computed:{:?}:{}", ty, Self::expr_cse_key(expr))
1202 }
1203 }
1204 }
1205
1206 fn const_cse_key(value: &xlog_ir::ConstValue) -> String {
1207 match value {
1208 xlog_ir::ConstValue::U32(value) => format!("u32:{value}"),
1209 xlog_ir::ConstValue::U64(value) => format!("u64:{value}"),
1210 xlog_ir::ConstValue::I32(value) => format!("i32:{value}"),
1211 xlog_ir::ConstValue::I64(value) => format!("i64:{value}"),
1212 xlog_ir::ConstValue::F32(value) => format!("f32:{:08x}", value.to_bits()),
1213 xlog_ir::ConstValue::F64(value) => format!("f64:{:016x}", value.to_bits()),
1214 xlog_ir::ConstValue::Bool(value) => format!("bool:{value}"),
1215 xlog_ir::ConstValue::Symbol(value) => format!("symbol:{value:?}"),
1216 }
1217 }
1218
1219 fn compare_op_cse_key(op: xlog_ir::CompareOp) -> &'static str {
1220 match op {
1221 xlog_ir::CompareOp::Eq => "eq",
1222 xlog_ir::CompareOp::Ne => "ne",
1223 xlog_ir::CompareOp::Lt => "lt",
1224 xlog_ir::CompareOp::Le => "le",
1225 xlog_ir::CompareOp::Gt => "gt",
1226 xlog_ir::CompareOp::Ge => "ge",
1227 }
1228 }
1229
1230 fn store_put(&mut self, name: &str, buffer: CudaBuffer) {
1231 self.common_subexpression_cache.clear();
1232 self.store.put(name, buffer);
1233 if let Some(&rel_id) = self.name_to_rel.get(name) {
1234 self.join_index_cache.invalidate_rel(rel_id);
1235 }
1236 }
1237
1238 fn store_remove(&mut self, name: &str) -> Option<CudaBuffer> {
1239 self.common_subexpression_cache.clear();
1240 if let Some(&rel_id) = self.name_to_rel.get(name) {
1241 self.join_index_cache.invalidate_rel(rel_id);
1242 }
1243 self.store.remove(name)
1244 }
1245
1246 pub fn register_relation(&mut self, rel_id: RelId, name: &str) {
1255 self.rel_names.insert(rel_id, name.to_string());
1256 self.name_to_rel.insert(name.to_string(), rel_id);
1257 self.stats.register_relation(rel_id);
1258 }
1259
1260 fn name_to_rel_id(&self, name: &str) -> Option<RelId> {
1268 self.name_to_rel.get(name).copied()
1269 }
1270
1271 fn get_rel_name(&self, rel_id: RelId) -> Option<&str> {
1273 self.rel_names.get(&rel_id).map(|s| s.as_str())
1274 }
1275
1276 pub fn execute_plan_with_adaptive_candidate(
1286 &mut self,
1287 baseline_plan: &ExecutionPlan,
1288 candidate_plan: &ExecutionPlan,
1289 ) -> Result<CudaBuffer> {
1290 self.adaptive_reoptimization_stats.invocations = self
1291 .adaptive_reoptimization_stats
1292 .invocations
1293 .saturating_add(1);
1294 self.adaptive_reoptimization_stats.diagnostics.clear();
1295 let before_dtoh_calls = self.provider.host_transfer_stats().dtoh_calls;
1296
1297 self.execute_plan(baseline_plan)?;
1298 let baseline_observations = self.adaptive_join_observations.clone();
1299 self.adaptive_reoptimization_stats.last_observations = baseline_observations.clone();
1300 let decision = self.adaptive_reoptimization_decision(&baseline_observations)?;
1301 self.adaptive_reoptimization_stats.last_decision = Some(decision.clone());
1302
1303 match decision.action {
1304 AdaptiveReoptimizationAction::Disabled => {
1305 self.adaptive_reoptimization_stats.disabled = self
1306 .adaptive_reoptimization_stats
1307 .disabled
1308 .saturating_add(1);
1309 self.record_adaptive_dtoh_delta(before_dtoh_calls);
1310 return self.clone_final_plan_output(baseline_plan);
1311 }
1312 AdaptiveReoptimizationAction::Skipped => {
1313 self.adaptive_reoptimization_stats.skipped =
1314 self.adaptive_reoptimization_stats.skipped.saturating_add(1);
1315 self.record_adaptive_dtoh_delta(before_dtoh_calls);
1316 return self.clone_final_plan_output(baseline_plan);
1317 }
1318 AdaptiveReoptimizationAction::AttemptCandidate => {}
1319 AdaptiveReoptimizationAction::Adopted | AdaptiveReoptimizationAction::RolledBack => {
1320 unreachable!("decision replay never returns terminal adaptive actions")
1321 }
1322 }
1323
1324 let head_names = Self::plan_head_names(baseline_plan);
1325 let baseline_snapshot = self.clone_store_snapshot()?;
1326 let baseline_stats_snapshot = self.stats_snapshot();
1327
1328 if let Err(err) = self.execute_plan(candidate_plan) {
1329 self.restore_store_snapshot(baseline_snapshot);
1330 self.restore_stats_snapshot(&baseline_stats_snapshot);
1331 self.adaptive_reoptimization_stats.rolled_back = self
1332 .adaptive_reoptimization_stats
1333 .rolled_back
1334 .saturating_add(1);
1335 self.adaptive_reoptimization_stats
1336 .diagnostics
1337 .push(AdaptiveReoptimizationDiagnostic {
1338 kind: AdaptiveReoptimizationDiagnosticKind::CandidateExecutionFailed,
1339 message: err.to_string(),
1340 });
1341 self.adaptive_reoptimization_stats
1342 .diagnostics
1343 .push(AdaptiveReoptimizationDiagnostic {
1344 kind: AdaptiveReoptimizationDiagnosticKind::RollbackRestoredBaseline,
1345 message: "baseline_snapshot_restored".to_string(),
1346 });
1347 self.adaptive_reoptimization_stats.last_observations = baseline_observations;
1348 self.adaptive_reoptimization_stats.last_decision =
1349 Some(AdaptiveReoptimizationDecision {
1350 action: AdaptiveReoptimizationAction::RolledBack,
1351 reason: "candidate_execution_failed".to_string(),
1352 max_misplan_ratio: decision.max_misplan_ratio,
1353 min_misplan_ratio: decision.min_misplan_ratio,
1354 });
1355 self.record_adaptive_dtoh_delta(before_dtoh_calls);
1356 return self.clone_final_plan_output(baseline_plan);
1357 }
1358
1359 if !self.plan_outputs_match(&head_names, &baseline_snapshot)? {
1360 self.restore_store_snapshot(baseline_snapshot);
1361 self.restore_stats_snapshot(&baseline_stats_snapshot);
1362 self.adaptive_reoptimization_stats.rolled_back = self
1363 .adaptive_reoptimization_stats
1364 .rolled_back
1365 .saturating_add(1);
1366 self.adaptive_reoptimization_stats
1367 .diagnostics
1368 .push(AdaptiveReoptimizationDiagnostic {
1369 kind: AdaptiveReoptimizationDiagnosticKind::CandidateOutputMismatch,
1370 message: "candidate_output_mismatch".to_string(),
1371 });
1372 self.adaptive_reoptimization_stats
1373 .diagnostics
1374 .push(AdaptiveReoptimizationDiagnostic {
1375 kind: AdaptiveReoptimizationDiagnosticKind::RollbackRestoredBaseline,
1376 message: "baseline_snapshot_restored".to_string(),
1377 });
1378 self.adaptive_reoptimization_stats.last_observations = baseline_observations;
1379 self.adaptive_reoptimization_stats.last_decision =
1380 Some(AdaptiveReoptimizationDecision {
1381 action: AdaptiveReoptimizationAction::RolledBack,
1382 reason: "candidate_output_mismatch".to_string(),
1383 max_misplan_ratio: decision.max_misplan_ratio,
1384 min_misplan_ratio: decision.min_misplan_ratio,
1385 });
1386 self.record_adaptive_dtoh_delta(before_dtoh_calls);
1387 return self.clone_final_plan_output(baseline_plan);
1388 }
1389
1390 self.adaptive_reoptimization_stats.adopted =
1391 self.adaptive_reoptimization_stats.adopted.saturating_add(1);
1392 self.adaptive_reoptimization_stats.last_observations = baseline_observations;
1393 self.adaptive_reoptimization_stats.last_decision = Some(AdaptiveReoptimizationDecision {
1394 action: AdaptiveReoptimizationAction::Adopted,
1395 reason: "candidate_adopted".to_string(),
1396 max_misplan_ratio: decision.max_misplan_ratio,
1397 min_misplan_ratio: decision.min_misplan_ratio,
1398 });
1399 self.record_adaptive_dtoh_delta(before_dtoh_calls);
1400 self.clone_final_plan_output(candidate_plan)
1401 }
1402
1403 pub fn execute_plan(&mut self, plan: &ExecutionPlan) -> Result<CudaBuffer> {
1417 self.adaptive_join_observations.clear();
1418 self.common_subexpression_cache.clear();
1419 let gate = self.config.strict_deterministic_d2h;
1426 let prev_gate = self.provider.strict_deterministic_d2h_enabled();
1427 if gate && !prev_gate {
1428 self.provider.reset_deterministic_d2h_violations();
1433 self.provider.enable_strict_deterministic_d2h();
1434 }
1435 let _gate_guard = D2hGateGuard {
1438 provider: Arc::clone(&self.provider),
1439 engaged: gate,
1440 previous: prev_gate,
1441 };
1442
1443 for (idx, stratum) in plan.strata.iter().enumerate() {
1445 let (num_rules, is_recursive) = stratum
1447 .sccs
1448 .iter()
1449 .map(|&scc_id| {
1450 let rules = plan
1451 .rules_by_scc
1452 .get(scc_id as usize)
1453 .map(|r| r.len())
1454 .unwrap_or(0);
1455 let recursive = plan
1456 .sccs
1457 .get(scc_id as usize)
1458 .map(|s| s.is_recursive)
1459 .unwrap_or(false);
1460 (rules, recursive)
1461 })
1462 .fold((0, false), |(r, rec), (nr, nrec)| (r + nr, rec || nrec));
1463
1464 self.profiler.begin_stratum(idx, num_rules, is_recursive);
1465 self.execute_stratum_impl(stratum, plan)?;
1466
1467 let mem_bytes = self.provider.memory().allocated_bytes();
1469 self.profiler.record_peak_memory(mem_bytes);
1470
1471 self.profiler.end_stratum();
1472 }
1473
1474 self.provider.device().synchronize()?;
1476 self.adaptive_reoptimization_stats.last_observations =
1477 self.adaptive_join_observations.clone();
1478
1479 self.provider.create_empty_buffer(Schema::new(vec![]))
1481 }
1482
1483 #[cfg(test)]
1495 fn evaluate_predicate(
1496 expr: &Expr,
1497 columns: &[Vec<u8>],
1498 row_idx: usize,
1499 schema: &Schema,
1500 ) -> Result<bool> {
1501 match expr {
1502 Expr::Column(col_idx) => {
1503 let col_type = schema.column_type(*col_idx);
1505 if let Some(ScalarType::Bool) = col_type {
1506 Ok(columns
1507 .get(*col_idx)
1508 .map(|c| c.get(row_idx).copied().unwrap_or(0) != 0)
1509 .unwrap_or(false))
1510 } else {
1511 Ok(true)
1513 }
1514 }
1515
1516 Expr::Const(ConstValue::Bool(b)) => Ok(*b),
1517 Expr::Const(_) => Ok(true), Expr::Compare { left, op, right } => {
1520 let use_float =
1521 Self::expr_may_be_float(left, schema) || Self::expr_may_be_float(right, schema);
1522
1523 if use_float {
1524 let left_val = f64_total_order_key(Self::evaluate_expr_as_f64(
1525 left, columns, row_idx, schema,
1526 )?);
1527 let right_val = f64_total_order_key(Self::evaluate_expr_as_f64(
1528 right, columns, row_idx, schema,
1529 )?);
1530
1531 Ok(match op {
1532 CompareOp::Eq => left_val == right_val,
1533 CompareOp::Ne => left_val != right_val,
1534 CompareOp::Lt => left_val < right_val,
1535 CompareOp::Le => left_val <= right_val,
1536 CompareOp::Gt => left_val > right_val,
1537 CompareOp::Ge => left_val >= right_val,
1538 })
1539 } else {
1540 let left_val = Self::evaluate_expr_as_i64(left, columns, row_idx, schema)?;
1541 let right_val = Self::evaluate_expr_as_i64(right, columns, row_idx, schema)?;
1542
1543 Ok(match op {
1544 CompareOp::Eq => left_val == right_val,
1545 CompareOp::Ne => left_val != right_val,
1546 CompareOp::Lt => left_val < right_val,
1547 CompareOp::Le => left_val <= right_val,
1548 CompareOp::Gt => left_val > right_val,
1549 CompareOp::Ge => left_val >= right_val,
1550 })
1551 }
1552 }
1553
1554 Expr::And(exprs) => {
1555 for e in exprs {
1556 if !Self::evaluate_predicate(e, columns, row_idx, schema)? {
1557 return Ok(false);
1558 }
1559 }
1560 Ok(true)
1561 }
1562
1563 Expr::Or(exprs) => {
1564 for e in exprs {
1565 if Self::evaluate_predicate(e, columns, row_idx, schema)? {
1566 return Ok(true);
1567 }
1568 }
1569 Ok(false)
1570 }
1571
1572 Expr::Not(inner) => Ok(!Self::evaluate_predicate(inner, columns, row_idx, schema)?),
1573
1574 Expr::Add(_, _)
1576 | Expr::Sub(_, _)
1577 | Expr::Mul(_, _)
1578 | Expr::Div(_, _)
1579 | Expr::Mod(_, _)
1580 | Expr::Abs(_)
1581 | Expr::Min(_, _)
1582 | Expr::Max(_, _)
1583 | Expr::Pow(_, _)
1584 | Expr::Cast(_, _)
1585 | Expr::Conditional { .. } => Err(XlogError::Execution(
1586 "Arithmetic expression cannot be evaluated as boolean predicate".into(),
1587 )),
1588 }
1589 }
1590
1591 #[cfg(test)]
1592 fn evaluate_expr_as_f64(
1593 expr: &Expr,
1594 columns: &[Vec<u8>],
1595 row_idx: usize,
1596 schema: &Schema,
1597 ) -> Result<f64> {
1598 match expr {
1599 Expr::Column(col_idx) => {
1600 let col_type = schema.column_type(*col_idx).unwrap_or(ScalarType::U32);
1601 let col_data = columns
1602 .get(*col_idx)
1603 .ok_or_else(|| XlogError::Execution(format!("Column {} not found", col_idx)))?;
1604
1605 let type_size = col_type.size_bytes();
1606 let start = row_idx * type_size;
1607
1608 Ok(match col_type {
1609 ScalarType::F64 => {
1610 let bytes = &col_data[start..start + 8];
1611 f64::from_le_bytes([
1612 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1613 bytes[7],
1614 ])
1615 }
1616 ScalarType::F32 => {
1617 let bytes = &col_data[start..start + 4];
1618 f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64
1619 }
1620 ScalarType::U32 | ScalarType::Symbol => {
1621 let bytes = &col_data[start..start + 4];
1622 u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64
1623 }
1624 ScalarType::I32 => {
1625 let bytes = &col_data[start..start + 4];
1626 i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64
1627 }
1628 ScalarType::U64 => {
1629 let bytes = &col_data[start..start + 8];
1630 u64::from_le_bytes([
1631 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1632 bytes[7],
1633 ]) as f64
1634 }
1635 ScalarType::I64 => {
1636 let bytes = &col_data[start..start + 8];
1637 i64::from_le_bytes([
1638 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1639 bytes[7],
1640 ]) as f64
1641 }
1642 ScalarType::Bool => col_data.get(start).copied().unwrap_or(0) as f64,
1643 })
1644 }
1645
1646 Expr::Const(val) => Ok(match val {
1647 ConstValue::U32(v) => *v as f64,
1648 ConstValue::I32(v) => *v as f64,
1649 ConstValue::U64(v) => *v as f64,
1650 ConstValue::I64(v) => *v as f64,
1651 ConstValue::Bool(b) => {
1652 if *b {
1653 1.0
1654 } else {
1655 0.0
1656 }
1657 }
1658 ConstValue::F32(f) => *f as f64,
1659 ConstValue::F64(f) => *f,
1660 ConstValue::Symbol(_) => {
1661 return Err(XlogError::Execution(
1662 "Cannot evaluate Symbol constant as f64".to_string(),
1663 ));
1664 }
1665 }),
1666
1667 Expr::Add(l, r) => Ok(Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?
1668 + Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?),
1669 Expr::Sub(l, r) => Ok(Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?
1670 - Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?),
1671 Expr::Mul(l, r) => Ok(Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?
1672 * Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?),
1673 Expr::Div(l, r) => {
1674 let left_val = Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?;
1675 let right_val = Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?;
1676 if right_val == 0.0 {
1677 return Err(XlogError::Execution("Division by zero".to_string()));
1678 }
1679 Ok(left_val / right_val)
1680 }
1681 Expr::Mod(l, r) => {
1682 let left_val = Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?;
1683 let right_val = Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?;
1684 if right_val == 0.0 {
1685 return Err(XlogError::Execution("Modulo by zero".to_string()));
1686 }
1687 Ok(left_val % right_val)
1688 }
1689 Expr::Abs(inner) => {
1690 Ok(Self::evaluate_expr_as_f64(inner, columns, row_idx, schema)?.abs())
1691 }
1692 Expr::Min(l, r) => Ok(Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?
1693 .min(Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?)),
1694 Expr::Max(l, r) => Ok(Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?
1695 .max(Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?)),
1696 Expr::Pow(base, exp) => Ok(Self::evaluate_expr_as_f64(base, columns, row_idx, schema)?
1697 .powf(Self::evaluate_expr_as_f64(exp, columns, row_idx, schema)?)),
1698 Expr::Cast(inner, target_type) => match target_type {
1699 ScalarType::F64 => Self::evaluate_expr_as_f64(inner, columns, row_idx, schema),
1700 ScalarType::F32 => {
1701 Ok(Self::evaluate_expr_as_f64(inner, columns, row_idx, schema)? as f32 as f64)
1702 }
1703 _ => Ok(Self::evaluate_expr_as_i64(inner, columns, row_idx, schema)? as f64),
1704 },
1705
1706 _ => Err(XlogError::Execution(
1707 "Cannot evaluate compound expression as f64".to_string(),
1708 )),
1709 }
1710 }
1711
1712 #[cfg(test)]
1714 fn evaluate_expr_as_i64(
1715 expr: &Expr,
1716 columns: &[Vec<u8>],
1717 row_idx: usize,
1718 schema: &Schema,
1719 ) -> Result<i64> {
1720 match expr {
1721 Expr::Column(col_idx) => {
1722 let col_type = schema.column_type(*col_idx).unwrap_or(ScalarType::U32);
1723 let col_data = columns
1724 .get(*col_idx)
1725 .ok_or_else(|| XlogError::Execution(format!("Column {} not found", col_idx)))?;
1726
1727 let type_size = col_type.size_bytes();
1728 let start = row_idx * type_size;
1729
1730 Ok(match col_type {
1731 ScalarType::U32 => {
1732 let bytes = &col_data[start..start + 4];
1733 u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as i64
1734 }
1735 ScalarType::I32 => {
1736 let bytes = &col_data[start..start + 4];
1737 i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as i64
1738 }
1739 ScalarType::U64 => {
1740 let bytes = &col_data[start..start + 8];
1741 u64::from_le_bytes([
1742 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1743 bytes[7],
1744 ]) as i64
1745 }
1746 ScalarType::I64 => {
1747 let bytes = &col_data[start..start + 8];
1748 i64::from_le_bytes([
1749 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1750 bytes[7],
1751 ])
1752 }
1753 ScalarType::Bool => col_data.get(start).copied().unwrap_or(0) as i64,
1754 ScalarType::Symbol => {
1755 let bytes = &col_data[start..start + 4];
1756 u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as i64
1757 }
1758 ScalarType::F32 => {
1759 let bytes = &col_data[start..start + 4];
1760 let val = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1761 val as i64
1762 }
1763 ScalarType::F64 => {
1764 let bytes = &col_data[start..start + 8];
1765 let val = f64::from_le_bytes([
1766 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1767 bytes[7],
1768 ]);
1769 val as i64
1770 }
1771 })
1772 }
1773
1774 Expr::Const(val) => Ok(match val {
1775 ConstValue::U32(v) => *v as i64,
1776 ConstValue::I32(v) => *v as i64,
1777 ConstValue::U64(v) => *v as i64,
1778 ConstValue::I64(v) => *v,
1779 ConstValue::Bool(b) => *b as i64,
1780 ConstValue::F32(f) => *f as i64,
1781 ConstValue::F64(f) => *f as i64,
1782 ConstValue::Symbol(_) => 0,
1783 }),
1784
1785 Expr::Add(l, r) => {
1787 let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1788 let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1789 Ok(left_val.wrapping_add(right_val))
1790 }
1791 Expr::Sub(l, r) => {
1792 let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1793 let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1794 Ok(left_val.wrapping_sub(right_val))
1795 }
1796 Expr::Mul(l, r) => {
1797 let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1798 let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1799 Ok(left_val.wrapping_mul(right_val))
1800 }
1801 Expr::Div(l, r) => {
1802 let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1803 let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1804 if right_val == 0 {
1805 return Err(XlogError::Execution("Division by zero".to_string()));
1806 }
1807 Ok(left_val / right_val)
1808 }
1809 Expr::Mod(l, r) => {
1810 let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1811 let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1812 if right_val == 0 {
1813 return Err(XlogError::Execution("Modulo by zero".to_string()));
1814 }
1815 Ok(left_val % right_val)
1816 }
1817 Expr::Abs(inner) => {
1818 let val = Self::evaluate_expr_as_i64(inner, columns, row_idx, schema)?;
1819 Ok(val.abs())
1820 }
1821 Expr::Min(l, r) => {
1822 let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1823 let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1824 Ok(left_val.min(right_val))
1825 }
1826 Expr::Max(l, r) => {
1827 let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1828 let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1829 Ok(left_val.max(right_val))
1830 }
1831 Expr::Pow(base, exp) => {
1832 let base_val = Self::evaluate_expr_as_i64(base, columns, row_idx, schema)?;
1833 let exp_val = Self::evaluate_expr_as_i64(exp, columns, row_idx, schema)?;
1834 if exp_val < 0 {
1835 Err(XlogError::Execution(
1836 "Negative exponent in integer pow".to_string(),
1837 ))
1838 } else if exp_val > u32::MAX as i64 {
1839 Ok(i64::MAX)
1841 } else {
1842 Ok(base_val.pow(exp_val as u32))
1843 }
1844 }
1845 Expr::Cast(inner, _target_type) => {
1846 Self::evaluate_expr_as_i64(inner, columns, row_idx, schema)
1848 }
1849
1850 _ => Err(XlogError::Execution(
1851 "Cannot evaluate compound expression as value".to_string(),
1852 )),
1853 }
1854 }
1855
1856 fn get_or_create_rel_name(&mut self, rel_id: RelId, default: &str) -> String {
1858 if let Some(name) = self.rel_names.get(&rel_id) {
1859 name.clone()
1860 } else {
1861 self.register_relation(rel_id, default);
1862 default.to_string()
1863 }
1864 }
1865
1866 fn create_empty_buffer(&self, schema: Schema) -> Result<CudaBuffer> {
1870 self.provider.create_empty_buffer(schema)
1871 }
1872
1873 fn clone_buffer(&self, buffer: &CudaBuffer) -> Result<CudaBuffer> {
1875 if buffer.is_empty() {
1876 return self.create_empty_buffer(buffer.schema().clone());
1877 }
1878
1879 let mut result_columns = Vec::with_capacity(buffer.arity());
1880
1881 for col_idx in 0..buffer.arity() {
1882 let col_type_size = buffer
1883 .schema()
1884 .column_type(col_idx)
1885 .map(|t| t.size_bytes())
1886 .unwrap_or(4);
1887 let bytes = (buffer.num_rows() as usize) * col_type_size;
1888
1889 if let Some(src_col) = buffer.column(col_idx) {
1890 let mut dst_col = self.provider.memory().alloc::<u8>(bytes)?;
1891 if bytes > 0 {
1892 self.provider
1893 .device()
1894 .inner()
1895 .dtod_copy(src_col, &mut dst_col)
1896 .map_err(|e| {
1897 XlogError::Execution(format!("Failed to clone column on device: {}", e))
1898 })?;
1899 }
1900 result_columns.push(dst_col.into());
1901 }
1902 }
1903
1904 let d_num_rows = self.clone_device_row_count(buffer)?;
1905 let cloned = match buffer.cached_row_count() {
1906 Some(row_count) => CudaBuffer::from_columns_with_host_count(
1907 result_columns,
1908 buffer.num_rows(),
1909 d_num_rows,
1910 buffer.schema().clone(),
1911 row_count,
1912 ),
1913 None => CudaBuffer::from_columns(
1914 result_columns,
1915 buffer.num_rows(),
1916 d_num_rows,
1917 buffer.schema().clone(),
1918 ),
1919 };
1920 Ok(cloned)
1921 }
1922
1923 fn clone_device_row_count(&self, buffer: &CudaBuffer) -> Result<TrackedCudaSlice<u32>> {
1924 let mut d_num_rows = self.provider.memory().alloc::<u32>(1)?;
1925 self.provider
1926 .device()
1927 .inner()
1928 .dtod_copy(buffer.num_rows_device(), &mut d_num_rows)
1929 .map_err(|e| XlogError::Execution(format!("Failed to copy row count: {}", e)))?;
1930 Ok(d_num_rows)
1931 }
1932
1933 fn buffer_row_count(&self, buffer: &CudaBuffer) -> Result<u32> {
1934 let n = self
1942 .provider
1943 .device_row_count(buffer)
1944 .map_err(|e| XlogError::Execution(format!("Failed to read row count: {}", e)))?;
1945 u32::try_from(n).map_err(|_| {
1946 XlogError::Execution(format!("Row count {n} exceeds the supported u32 range"))
1947 })
1948 }
1949}
1950
1951struct D2hGateGuard {
1955 provider: Arc<CudaKernelProvider>,
1956 engaged: bool,
1957 previous: bool,
1958}
1959
1960impl Drop for D2hGateGuard {
1961 fn drop(&mut self) {
1962 if !self.engaged {
1963 return;
1964 }
1965 if self.previous {
1966 self.provider.enable_strict_deterministic_d2h();
1967 } else {
1968 self.provider.disable_strict_deterministic_d2h();
1969 }
1970 }
1971}
1972
1973#[cfg(test)]
1974mod tests {
1975 use super::*;
1976 use std::time::{Duration, Instant};
1977 use xlog_core::MemoryBudget;
1978 use xlog_cuda::{CudaDevice, CudaProviderBuilder, GpuMemoryManager};
1979 use xlog_ir::{CompiledRule, RirMeta, Scc};
1980
1981 fn has_cuda_device() -> bool {
1982 CudaDevice::new(0).is_ok()
1984 }
1985
1986 fn create_test_executor() -> Option<Executor> {
1987 if !has_cuda_device() {
1988 return None;
1989 }
1990 let budget = MemoryBudget::with_limit(1024 * 1024 * 1024); let provider = Arc::new(CudaProviderBuilder::new(0, budget).build().ok()?);
1992 Some(Executor::new(provider))
1993 }
1994
1995 fn create_test_executor_with_config(config: RuntimeConfig) -> Option<Executor> {
1996 if !has_cuda_device() {
1997 return None;
1998 }
1999 let budget = MemoryBudget::with_limit(1024 * 1024 * 1024); let provider = Arc::new(CudaProviderBuilder::new(0, budget).build().ok()?);
2001 Some(Executor::new_with_config(provider, config))
2002 }
2003
2004 fn create_manager_stats_fixture() -> Option<(Arc<GpuMemoryManager>, Arc<CudaKernelProvider>)> {
2005 let provider =
2006 match CudaProviderBuilder::new(0, MemoryBudget::with_limit(1024 * 1024 * 1024)).build()
2007 {
2008 Ok(provider) => Arc::new(provider),
2009 Err(error) if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") => {
2010 panic!("XLOG_REQUIRE_CUDA=1 but provider initialization failed: {error}")
2011 }
2012 Err(error) => {
2013 eprintln!("Skipping test: provider initialization failed: {error}");
2014 return None;
2015 }
2016 };
2017 let memory = Arc::clone(provider.memory());
2018 Some((memory, provider))
2019 }
2020
2021 #[test]
2022 fn manager_reservation_peak_transient_drop_before_stats() {
2023 let Some((memory, provider)) = create_manager_stats_fixture() else {
2024 return;
2025 };
2026 let mut executor = Executor::new(provider);
2027 executor.set_profiling(true);
2028 let baseline = memory.allocated_bytes();
2029 let transient = memory.alloc::<u8>(4096).expect("transient allocation");
2030 let expected_peak = baseline + 4096;
2031 drop(transient);
2032 assert_eq!(memory.allocated_bytes(), baseline);
2033
2034 assert_eq!(
2035 executor.execution_stats(0).peak_memory_bytes,
2036 expected_peak,
2037 "stats must retain a transient reservation dropped before sampling"
2038 );
2039 }
2040
2041 #[test]
2042 fn manager_reservation_peak_enabled_no_op_preserves_provider_history() {
2043 let Some((memory, provider)) = create_manager_stats_fixture() else {
2044 return;
2045 };
2046 let baseline = memory.allocated_bytes();
2047 let transient = memory.alloc::<u8>(2048).expect("historical allocation");
2048 let expected_peak = baseline + 2048;
2049 drop(transient);
2050
2051 let mut executor = Executor::new(provider);
2052 executor.set_profiling(true);
2053 assert_eq!(
2054 executor.execution_stats(0).peak_memory_bytes,
2055 expected_peak,
2056 "an enabled executor with no operations must expose provider history"
2057 );
2058 }
2059
2060 #[test]
2061 fn manager_reservation_peak_disabled_stats_still_report_manager_peak() {
2062 let Some((memory, provider)) = create_manager_stats_fixture() else {
2063 return;
2064 };
2065 let baseline = memory.allocated_bytes();
2066 let transient = memory.alloc::<u8>(3072).expect("transient allocation");
2067 let expected_peak = baseline + 3072;
2068 drop(transient);
2069
2070 let executor = Executor::new(provider);
2071 assert!(!executor.is_profiling());
2072 assert_eq!(
2073 executor.execution_stats(0).peak_memory_bytes,
2074 expected_peak,
2075 "manager peak is available independently of profiler sampling"
2076 );
2077 }
2078
2079 #[test]
2080 fn manager_reservation_peak_shared_manager_has_provider_lifetime_scope() {
2081 let Some((memory, provider)) = create_manager_stats_fixture() else {
2082 return;
2083 };
2084 let mut first = Executor::new(Arc::clone(&provider));
2085 first.set_profiling(true);
2086 let baseline = memory.allocated_bytes();
2087 let transient = memory
2088 .alloc::<u8>(5120)
2089 .expect("shared transient allocation");
2090 let expected_peak = baseline + 5120;
2091 drop(transient);
2092 assert_eq!(first.execution_stats(0).peak_memory_bytes, expected_peak);
2093
2094 let mut second = Executor::new(provider);
2095 second.set_profiling(true);
2096 assert_eq!(
2097 second.execution_stats(0).peak_memory_bytes,
2098 expected_peak,
2099 "constructing another executor must not reset the shared manager"
2100 );
2101 }
2102
2103 #[test]
2104 fn manager_reservation_peak_quiescent_reset_starts_new_window() {
2105 let Some((memory, provider)) = create_manager_stats_fixture() else {
2106 return;
2107 };
2108 let old = memory.alloc::<u8>(4096).expect("old-window allocation");
2109 drop(old);
2110 assert_eq!(memory.allocated_bytes(), 0, "reset requires quiescence");
2111 memory.reset_peak();
2112 assert_eq!(memory.peak_bytes(), 0);
2113
2114 let new = memory.alloc::<u8>(1024).expect("new-window allocation");
2115 drop(new);
2116 let executor = Executor::new(provider);
2117 assert_eq!(
2118 executor.execution_stats(0).peak_memory_bytes,
2119 1024,
2120 "stats must reflect the explicit reset window"
2121 );
2122 }
2123
2124 fn device_row_count(executor: &Executor, rows: u64) -> TrackedCudaSlice<u32> {
2125 let rows_u32 = u32::try_from(rows).expect("row count fits u32");
2126 let mut d_num_rows = executor.provider.memory().alloc::<u32>(1).expect("alloc");
2127 executor
2128 .provider
2129 .device()
2130 .inner()
2131 .htod_sync_copy_into(&[rows_u32], &mut d_num_rows)
2132 .expect("htod");
2133 d_num_rows
2134 }
2135
2136 fn create_test_buffer(executor: &Executor, data: &[u32], col_name: &str) -> CudaBuffer {
2137 let schema = Schema::new(vec![(col_name.to_string(), ScalarType::U32)]);
2138 let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_le_bytes()).collect();
2139
2140 let mut col = executor
2141 .provider
2142 .memory()
2143 .alloc::<u8>(bytes.len())
2144 .expect("alloc");
2145 executor
2146 .provider
2147 .device()
2148 .inner()
2149 .htod_sync_copy_into(&bytes, &mut col)
2150 .expect("htod");
2151
2152 let rows = data.len() as u64;
2153 let d_num_rows = device_row_count(executor, rows);
2154 CudaBuffer::from_columns(vec![col.into()], rows, d_num_rows, schema)
2155 }
2156
2157 #[test]
2158 fn clone_buffer_preserves_cached_logical_rows_without_host_transfer() {
2159 let Some((_memory, provider)) = create_manager_stats_fixture() else {
2160 return;
2161 };
2162 let executor = Executor::new(provider);
2163 let schema = Schema::new(vec![("value".to_string(), ScalarType::U32)]);
2164 let values = [10u32, 20, 99];
2165 let bytes: Vec<u8> = values
2166 .iter()
2167 .flat_map(|value| value.to_le_bytes())
2168 .collect();
2169 let mut column = executor
2170 .provider
2171 .memory()
2172 .alloc::<u8>(bytes.len())
2173 .expect("column allocation");
2174 executor
2175 .provider
2176 .device()
2177 .inner()
2178 .htod_sync_copy_into(&bytes, &mut column)
2179 .expect("column upload");
2180 let source = CudaBuffer::from_columns_with_host_count(
2181 vec![column.into()],
2182 values.len() as u64,
2183 device_row_count(&executor, 2),
2184 schema,
2185 2,
2186 );
2187
2188 executor.provider.reset_host_transfer_stats();
2189 let cloned = executor.clone_buffer(&source).expect("device clone");
2190 let clone_transfers = executor.provider.host_transfer_stats();
2191
2192 assert_eq!(cloned.cached_row_count(), Some(2));
2193 assert_eq!(clone_transfers.dtoh_calls, 0);
2194 assert_eq!(read_buffer_u32(&executor, &cloned, 0), vec![10, 20]);
2195
2196 let uncached = create_test_buffer(&executor, &[30, 40], "value");
2197 assert_eq!(uncached.cached_row_count(), None);
2198 executor.provider.reset_host_transfer_stats();
2199 let uncached_clone = executor
2200 .clone_buffer(&uncached)
2201 .expect("uncached device clone");
2202 assert_eq!(uncached_clone.cached_row_count(), None);
2203 assert_eq!(executor.provider.host_transfer_stats().dtoh_calls, 0);
2204 }
2205
2206 fn read_buffer_u32(executor: &Executor, buffer: &CudaBuffer, col: usize) -> Vec<u32> {
2207 executor
2208 .provider
2209 .download_column::<u32>(buffer, col)
2210 .unwrap_or_default()
2211 }
2212
2213 fn buffer_row_count(executor: &Executor, buffer: &CudaBuffer) -> u32 {
2214 executor
2215 .provider
2216 .device_row_count(buffer)
2217 .expect("dtoh row count") as u32
2218 }
2219
2220 fn to_f64_column_bytes(values: &[f64]) -> Vec<u8> {
2221 values.iter().flat_map(|v| v.to_le_bytes()).collect()
2222 }
2223
2224 fn to_f32_column_bytes(values: &[f32]) -> Vec<u8> {
2225 values.iter().flat_map(|v| v.to_le_bytes()).collect()
2226 }
2227
2228 #[test]
2231 fn test_executor_creation() {
2232 let executor = match create_test_executor() {
2233 Some(e) => e,
2234 None => {
2235 eprintln!("Skipping test: no CUDA device available");
2236 return;
2237 }
2238 };
2239
2240 assert!(executor.store().is_empty());
2241 }
2242
2243 #[test]
2244 fn test_predicate_f64_comparisons() {
2245 let schema = Schema::new(vec![("x".to_string(), ScalarType::F64)]);
2246 let values = [1.0f64, 2.0, 3.0, f64::NAN];
2247 let columns = vec![to_f64_column_bytes(&values)];
2248
2249 let gt_two = Expr::Compare {
2250 left: Box::new(Expr::Column(0)),
2251 op: CompareOp::Gt,
2252 right: Box::new(Expr::Const(ConstValue::F64(2.0))),
2253 };
2254
2255 let results: Vec<bool> = (0..values.len())
2256 .map(|row| Executor::evaluate_predicate(>_two, &columns, row, &schema).unwrap())
2257 .collect();
2258 assert_eq!(results, vec![false, false, true, true]);
2259
2260 let eq_nan = Expr::Compare {
2261 left: Box::new(Expr::Column(0)),
2262 op: CompareOp::Eq,
2263 right: Box::new(Expr::Const(ConstValue::F64(f64::NAN))),
2264 };
2265 let results: Vec<bool> = (0..values.len())
2266 .map(|row| Executor::evaluate_predicate(&eq_nan, &columns, row, &schema).unwrap())
2267 .collect();
2268 assert_eq!(results, vec![false, false, false, true]);
2269
2270 let ne_nan = Expr::Compare {
2271 left: Box::new(Expr::Column(0)),
2272 op: CompareOp::Ne,
2273 right: Box::new(Expr::Const(ConstValue::F64(f64::NAN))),
2274 };
2275 let results: Vec<bool> = (0..values.len())
2276 .map(|row| Executor::evaluate_predicate(&ne_nan, &columns, row, &schema).unwrap())
2277 .collect();
2278 assert_eq!(results, vec![true, true, true, false]);
2279 }
2280
2281 #[test]
2282 fn test_predicate_f32_comparisons() {
2283 let schema = Schema::new(vec![("x".to_string(), ScalarType::F32)]);
2284 let values = [1.0f32, 2.0, 3.0, f32::NAN];
2285 let columns = vec![to_f32_column_bytes(&values)];
2286
2287 let le_two = Expr::Compare {
2288 left: Box::new(Expr::Column(0)),
2289 op: CompareOp::Le,
2290 right: Box::new(Expr::Const(ConstValue::F32(2.0))),
2291 };
2292
2293 let results: Vec<bool> = (0..values.len())
2294 .map(|row| Executor::evaluate_predicate(&le_two, &columns, row, &schema).unwrap())
2295 .collect();
2296 assert_eq!(results, vec![true, true, false, false]);
2297 }
2298
2299 #[test]
2300 fn test_predicate_mixed_float_int_comparisons() {
2301 let schema = Schema::new(vec![
2302 ("x".to_string(), ScalarType::F64),
2303 ("y".to_string(), ScalarType::U32),
2304 ]);
2305
2306 let x = [1.5f64, 2.0, 2.5];
2307 let y = [1u32, 2, 3];
2308 let columns = vec![
2309 to_f64_column_bytes(&x),
2310 y.iter().flat_map(|v| v.to_le_bytes()).collect(),
2311 ];
2312
2313 let x_gt_2 = Expr::Compare {
2314 left: Box::new(Expr::Column(0)),
2315 op: CompareOp::Gt,
2316 right: Box::new(Expr::Const(ConstValue::U32(2))),
2317 };
2318 let results: Vec<bool> = (0..x.len())
2319 .map(|row| Executor::evaluate_predicate(&x_gt_2, &columns, row, &schema).unwrap())
2320 .collect();
2321 assert_eq!(results, vec![false, false, true]);
2322
2323 let y_lt_2_5 = Expr::Compare {
2324 left: Box::new(Expr::Column(1)),
2325 op: CompareOp::Lt,
2326 right: Box::new(Expr::Const(ConstValue::F64(2.5))),
2327 };
2328 let results: Vec<bool> = (0..y.len())
2329 .map(|row| Executor::evaluate_predicate(&y_lt_2_5, &columns, row, &schema).unwrap())
2330 .collect();
2331 assert_eq!(results, vec![true, true, false]);
2332 }
2333
2334 #[test]
2335 fn test_register_and_get_relation() {
2336 let mut executor = match create_test_executor() {
2337 Some(e) => e,
2338 None => {
2339 eprintln!("Skipping test: no CUDA device available");
2340 return;
2341 }
2342 };
2343
2344 executor.register_relation(RelId(1), "test_rel");
2346
2347 assert_eq!(executor.get_rel_name(RelId(1)), Some("test_rel"));
2349 assert_eq!(executor.get_rel_name(RelId(2)), None);
2350 }
2351
2352 #[test]
2355 fn test_execute_scan_not_found() {
2356 let mut executor = match create_test_executor() {
2357 Some(e) => e,
2358 None => {
2359 eprintln!("Skipping test: no CUDA device available");
2360 return;
2361 }
2362 };
2363
2364 executor.register_relation(RelId(1), "missing_rel");
2365
2366 let node = RirNode::Scan { rel: RelId(1) };
2367 let result = executor.execute_node(&node);
2368
2369 assert!(result.is_err());
2370 }
2371
2372 #[test]
2373 fn test_execute_scan_success() {
2374 let mut executor = match create_test_executor() {
2375 Some(e) => e,
2376 None => {
2377 eprintln!("Skipping test: no CUDA device available");
2378 return;
2379 }
2380 };
2381
2382 let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2384 executor.store_mut().put("test_rel", buffer);
2385 executor.register_relation(RelId(1), "test_rel");
2386
2387 let node = RirNode::Scan { rel: RelId(1) };
2389 let result = executor.execute_node(&node);
2390
2391 assert!(result.is_ok());
2392 let result = result.unwrap();
2393 assert_eq!(buffer_row_count(&executor, &result), 5);
2394
2395 let values = read_buffer_u32(&executor, &result, 0);
2396 assert_eq!(values, vec![1, 2, 3, 4, 5]);
2397 }
2398
2399 #[test]
2402 fn test_execute_filter_empty_input() {
2403 let executor = match create_test_executor() {
2404 Some(e) => e,
2405 None => {
2406 eprintln!("Skipping test: no CUDA device available");
2407 return;
2408 }
2409 };
2410
2411 let schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
2412 let empty = executor.create_empty_buffer(schema).unwrap();
2413
2414 let predicate = Expr::Const(ConstValue::Bool(true));
2415 let result = executor.execute_filter(&empty, &predicate);
2416
2417 assert!(result.is_ok());
2418 let result = result.unwrap();
2419 assert_eq!(buffer_row_count(&executor, &result), 0);
2420 }
2421
2422 #[test]
2423 fn test_execute_filter_all_match() {
2424 let executor = match create_test_executor() {
2425 Some(e) => e,
2426 None => {
2427 eprintln!("Skipping test: no CUDA device available");
2428 return;
2429 }
2430 };
2431
2432 let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2433 let predicate = Expr::Const(ConstValue::Bool(true));
2434
2435 let result = executor.execute_filter(&buffer, &predicate);
2436 assert!(result.is_ok());
2437
2438 let result = result.unwrap();
2439 assert_eq!(buffer_row_count(&executor, &result), 5);
2440 }
2441
2442 #[test]
2443 fn test_execute_filter_none_match() {
2444 let executor = match create_test_executor() {
2445 Some(e) => e,
2446 None => {
2447 eprintln!("Skipping test: no CUDA device available");
2448 return;
2449 }
2450 };
2451
2452 let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2453 let predicate = Expr::Const(ConstValue::Bool(false));
2454
2455 let result = executor.execute_filter(&buffer, &predicate);
2456 assert!(result.is_ok());
2457 let result = result.unwrap();
2458 assert_eq!(buffer_row_count(&executor, &result), 0);
2459 }
2460
2461 #[test]
2462 fn test_execute_filter_comparison() {
2463 let executor = match create_test_executor() {
2464 Some(e) => e,
2465 None => {
2466 eprintln!("Skipping test: no CUDA device available");
2467 return;
2468 }
2469 };
2470
2471 let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2472
2473 let predicate = Expr::Compare {
2475 left: Box::new(Expr::Column(0)),
2476 op: CompareOp::Gt,
2477 right: Box::new(Expr::Const(ConstValue::U32(3))),
2478 };
2479
2480 let result = executor.execute_filter(&buffer, &predicate);
2481 assert!(result.is_ok());
2482
2483 let result = result.unwrap();
2484 assert_eq!(buffer_row_count(&executor, &result), 2);
2485
2486 let values = read_buffer_u32(&executor, &result, 0);
2487 assert_eq!(values, vec![4, 5]);
2488 }
2489
2490 #[test]
2491 fn test_execute_filter_and() {
2492 let executor = match create_test_executor() {
2493 Some(e) => e,
2494 None => {
2495 eprintln!("Skipping test: no CUDA device available");
2496 return;
2497 }
2498 };
2499
2500 let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2501
2502 let predicate = Expr::And(vec![
2504 Expr::Compare {
2505 left: Box::new(Expr::Column(0)),
2506 op: CompareOp::Ge,
2507 right: Box::new(Expr::Const(ConstValue::U32(2))),
2508 },
2509 Expr::Compare {
2510 left: Box::new(Expr::Column(0)),
2511 op: CompareOp::Le,
2512 right: Box::new(Expr::Const(ConstValue::U32(4))),
2513 },
2514 ]);
2515
2516 let result = executor.execute_filter(&buffer, &predicate);
2517 assert!(result.is_ok());
2518
2519 let result = result.unwrap();
2520 assert_eq!(buffer_row_count(&executor, &result), 3);
2521
2522 let values = read_buffer_u32(&executor, &result, 0);
2523 assert_eq!(values, vec![2, 3, 4]);
2524 }
2525
2526 #[test]
2529 fn test_execute_project_empty_input() {
2530 let executor = match create_test_executor() {
2531 Some(e) => e,
2532 None => {
2533 eprintln!("Skipping test: no CUDA device available");
2534 return;
2535 }
2536 };
2537
2538 let schema = Schema::new(vec![
2539 ("a".to_string(), ScalarType::U32),
2540 ("b".to_string(), ScalarType::U32),
2541 ]);
2542 let empty = executor.create_empty_buffer(schema).unwrap();
2543
2544 let result = executor.execute_project(&empty, &[ProjectExpr::Column(0)]);
2545 assert!(result.is_ok());
2546
2547 let result = result.unwrap();
2548 assert_eq!(buffer_row_count(&executor, &result), 0);
2549 assert_eq!(result.arity(), 1);
2550 }
2551
2552 #[test]
2553 fn test_execute_zero_column_project_preserves_row_existence() {
2554 let executor = match create_test_executor() {
2555 Some(e) => e,
2556 None => {
2557 eprintln!("Skipping test: no CUDA device available");
2558 return;
2559 }
2560 };
2561
2562 let input = create_test_buffer(&executor, &[1, 2, 3], "key");
2563 let result = executor
2564 .execute_project(&input, &[])
2565 .expect("zero-column projection must execute");
2566
2567 assert_eq!(result.arity(), 0);
2568 assert_eq!(buffer_row_count(&executor, &result), 3);
2569 }
2570
2571 #[test]
2572 fn test_execute_project_reorder() {
2573 let executor = match create_test_executor() {
2574 Some(e) => e,
2575 None => {
2576 eprintln!("Skipping test: no CUDA device available");
2577 return;
2578 }
2579 };
2580
2581 let schema = Schema::new(vec![
2583 ("a".to_string(), ScalarType::U32),
2584 ("b".to_string(), ScalarType::U32),
2585 ]);
2586
2587 let a_data: Vec<u8> = [1u32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
2588 let b_data: Vec<u8> = [10u32, 20, 30]
2589 .iter()
2590 .flat_map(|v| v.to_le_bytes())
2591 .collect();
2592
2593 let mut col_a = executor
2594 .provider
2595 .memory()
2596 .alloc::<u8>(a_data.len())
2597 .unwrap();
2598 let mut col_b = executor
2599 .provider
2600 .memory()
2601 .alloc::<u8>(b_data.len())
2602 .unwrap();
2603
2604 executor
2605 .provider
2606 .device()
2607 .inner()
2608 .htod_sync_copy_into(&a_data, &mut col_a)
2609 .unwrap();
2610 executor
2611 .provider
2612 .device()
2613 .inner()
2614 .htod_sync_copy_into(&b_data, &mut col_b)
2615 .unwrap();
2616
2617 let d_num_rows = device_row_count(&executor, 3);
2618 let buffer =
2619 CudaBuffer::from_columns(vec![col_a.into(), col_b.into()], 3, d_num_rows, schema);
2620
2621 let result =
2623 executor.execute_project(&buffer, &[ProjectExpr::Column(1), ProjectExpr::Column(0)]);
2624 assert!(result.is_ok());
2625
2626 let result = result.unwrap();
2627 assert_eq!(buffer_row_count(&executor, &result), 3);
2628 assert_eq!(result.arity(), 2);
2629
2630 let col0 = read_buffer_u32(&executor, &result, 0);
2632 assert_eq!(col0, vec![10, 20, 30]);
2633
2634 let col1 = read_buffer_u32(&executor, &result, 1);
2636 assert_eq!(col1, vec![1, 2, 3]);
2637 }
2638
2639 #[test]
2640 fn test_execute_computed_projection_wiring() {
2641 let executor = match create_test_executor() {
2644 Some(e) => e,
2645 None => {
2646 eprintln!("Skipping test: no CUDA device available");
2647 return;
2648 }
2649 };
2650
2651 let schema = Schema::new(vec![
2653 ("a".to_string(), ScalarType::U32),
2654 ("b".to_string(), ScalarType::U32),
2655 ]);
2656
2657 let a_data: Vec<u8> = [10u32, 20, 30]
2658 .iter()
2659 .flat_map(|v| v.to_le_bytes())
2660 .collect();
2661 let b_data: Vec<u8> = [1u32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
2662
2663 let mut col_a = executor
2664 .provider
2665 .memory()
2666 .alloc::<u8>(a_data.len())
2667 .unwrap();
2668 let mut col_b = executor
2669 .provider
2670 .memory()
2671 .alloc::<u8>(b_data.len())
2672 .unwrap();
2673
2674 executor
2675 .provider
2676 .device()
2677 .inner()
2678 .htod_sync_copy_into(&a_data, &mut col_a)
2679 .unwrap();
2680 executor
2681 .provider
2682 .device()
2683 .inner()
2684 .htod_sync_copy_into(&b_data, &mut col_b)
2685 .unwrap();
2686
2687 let d_num_rows = device_row_count(&executor, 3);
2688 let buffer =
2689 CudaBuffer::from_columns(vec![col_a.into(), col_b.into()], 3, d_num_rows, schema);
2690
2691 let add_expr = Expr::Add(Box::new(Expr::Column(0)), Box::new(Expr::Column(1)));
2693 let projections = vec![
2694 ProjectExpr::Column(0), ProjectExpr::Computed(add_expr, ScalarType::U32), ];
2697
2698 let result = executor.execute_project(&buffer, &projections);
2699
2700 match result {
2704 Ok(res) => {
2705 assert_eq!(buffer_row_count(&executor, &res), 3);
2707 assert_eq!(res.arity(), 2);
2708
2709 let col0 = read_buffer_u32(&executor, &res, 0);
2711 assert_eq!(col0, vec![10, 20, 30]);
2712
2713 let col1 = read_buffer_u32(&executor, &res, 1);
2715 assert_eq!(col1, vec![11, 22, 33]);
2716 }
2717 Err(e) => {
2718 let err_msg = format!("{}", e);
2721 assert!(
2722 err_msg.contains("not implemented")
2723 || err_msg.contains("not yet implemented")
2724 || err_msg.contains("not supported")
2725 || err_msg.contains("stub")
2726 || err_msg.contains("Unsupported")
2727 || err_msg.contains("arithmetic kernels"),
2728 "Unexpected error: {}. Expected arithmetic kernel stub error.",
2729 err_msg
2730 );
2731 }
2732 }
2733 }
2734
2735 #[test]
2736 fn arithmetic_expression_evaluation_reports_left_operand_error_first() {
2737 let executor = match create_test_executor() {
2738 Some(executor) => executor,
2739 None => {
2740 eprintln!("Skipping test: no CUDA device available");
2741 return;
2742 }
2743 };
2744 let input = create_test_buffer(&executor, &[1], "key");
2745 let expression = Expr::Add(Box::new(Expr::Column(99)), Box::new(Expr::Column(98)));
2746
2747 let error = match executor.evaluate_arith_expr(&expression, &input) {
2748 Ok(_) => panic!("invalid left operand must fail"),
2749 Err(error) => error,
2750 };
2751
2752 assert!(error.to_string().contains("Column 99 not found"), "{error}");
2753 }
2754
2755 #[test]
2756 fn conditional_expression_evaluation_reports_condition_error_before_branches() {
2757 let executor = match create_test_executor() {
2758 Some(executor) => executor,
2759 None => {
2760 eprintln!("Skipping test: no CUDA device available");
2761 return;
2762 }
2763 };
2764 let input = create_test_buffer(&executor, &[1], "key");
2765 let expression = Expr::Conditional {
2766 condition: Box::new(Expr::Column(99)),
2767 then_expr: Box::new(Expr::Column(98)),
2768 else_expr: Box::new(Expr::Column(97)),
2769 };
2770
2771 let error = match executor.evaluate_arith_expr(&expression, &input) {
2772 Ok(_) => panic!("invalid condition must fail"),
2773 Err(error) => error,
2774 };
2775
2776 assert!(error.to_string().contains("Column 99 not found"), "{error}");
2777 }
2778
2779 #[test]
2780 fn conditional_expression_eagerly_evaluates_then_before_else() {
2781 let executor = match create_test_executor() {
2782 Some(executor) => executor,
2783 None => {
2784 eprintln!("Skipping test: no CUDA device available");
2785 return;
2786 }
2787 };
2788 let input = create_test_buffer(&executor, &[1], "key");
2789 let invalid_then = Expr::Conditional {
2790 condition: Box::new(Expr::Const(ConstValue::Bool(false))),
2791 then_expr: Box::new(Expr::Column(98)),
2792 else_expr: Box::new(Expr::Column(97)),
2793 };
2794 let error = match executor.evaluate_arith_expr(&invalid_then, &input) {
2795 Ok(_) => panic!("eager invalid then branch must fail"),
2796 Err(error) => error,
2797 };
2798 assert!(error.to_string().contains("Column 98 not found"), "{error}");
2799
2800 let invalid_else = Expr::Conditional {
2801 condition: Box::new(Expr::Const(ConstValue::Bool(true))),
2802 then_expr: Box::new(Expr::Column(0)),
2803 else_expr: Box::new(Expr::Column(97)),
2804 };
2805 let error = match executor.evaluate_arith_expr(&invalid_else, &input) {
2806 Ok(_) => panic!("eager invalid else branch must fail"),
2807 Err(error) => error,
2808 };
2809 assert!(error.to_string().contains("Column 97 not found"), "{error}");
2810 }
2811
2812 #[test]
2815 fn test_execute_union_empty_inputs() {
2816 let executor = match create_test_executor() {
2817 Some(e) => e,
2818 None => {
2819 eprintln!("Skipping test: no CUDA device available");
2820 return;
2821 }
2822 };
2823
2824 let result = executor.execute_union(&[]);
2825 assert!(result.is_ok());
2826 let result = result.unwrap();
2827 assert_eq!(buffer_row_count(&executor, &result), 0);
2828 }
2829
2830 #[test]
2831 fn test_execute_union_single_input() {
2832 let executor = match create_test_executor() {
2833 Some(e) => e,
2834 None => {
2835 eprintln!("Skipping test: no CUDA device available");
2836 return;
2837 }
2838 };
2839
2840 let buffer = create_test_buffer(&executor, &[1, 2, 3], "key");
2841
2842 let result = executor.execute_union(&[buffer]);
2843 assert!(result.is_ok());
2844
2845 let result = result.unwrap();
2846 assert_eq!(buffer_row_count(&executor, &result), 3);
2847
2848 let values = read_buffer_u32(&executor, &result, 0);
2849 assert_eq!(values, vec![1, 2, 3]);
2850 }
2851
2852 #[test]
2853 fn test_execute_union_multiple_inputs() {
2854 let executor = match create_test_executor() {
2855 Some(e) => e,
2856 None => {
2857 eprintln!("Skipping test: no CUDA device available");
2858 return;
2859 }
2860 };
2861
2862 let buffer1 = create_test_buffer(&executor, &[1, 2], "key");
2863 let buffer2 = create_test_buffer(&executor, &[3, 4], "key");
2864 let buffer3 = create_test_buffer(&executor, &[5], "key");
2865
2866 let result = executor.execute_union(&[buffer1, buffer2, buffer3]);
2867 assert!(result.is_ok());
2868
2869 let result = result.unwrap();
2870 assert_eq!(buffer_row_count(&executor, &result), 5);
2871 }
2872
2873 #[test]
2876 fn test_execute_distinct_empty() {
2877 let executor = match create_test_executor() {
2878 Some(e) => e,
2879 None => {
2880 eprintln!("Skipping test: no CUDA device available");
2881 return;
2882 }
2883 };
2884
2885 let schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
2886 let empty = executor.create_empty_buffer(schema).unwrap();
2887
2888 let result = executor.execute_distinct(&empty, &[0]);
2889 assert!(result.is_ok());
2890 let result = result.unwrap();
2891 assert_eq!(buffer_row_count(&executor, &result), 0);
2892 }
2893
2894 #[test]
2897 fn test_execute_diff() {
2898 let executor = match create_test_executor() {
2899 Some(e) => e,
2900 None => {
2901 eprintln!("Skipping test: no CUDA device available");
2902 return;
2903 }
2904 };
2905
2906 let left = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2907 let right = create_test_buffer(&executor, &[2, 4], "key");
2908
2909 let result = executor.execute_diff(&left, &right);
2910 assert!(result.is_ok());
2911
2912 let result = result.unwrap();
2913 assert_eq!(buffer_row_count(&executor, &result), 3);
2914
2915 let values = read_buffer_u32(&executor, &result, 0);
2916 assert_eq!(values, vec![1, 3, 5]);
2917 }
2918
2919 #[test]
2922 fn test_execute_fixpoint_base_only() {
2923 let mut executor = match create_test_executor() {
2926 Some(e) => e,
2927 None => {
2928 eprintln!("Skipping test: no CUDA device available");
2929 return;
2930 }
2931 };
2932
2933 let buffer = create_test_buffer(&executor, &[1, 2, 3], "key");
2935 executor.store_mut().put("base_rel", buffer);
2936 executor.register_relation(RelId(1), "base_rel");
2937
2938 let empty_schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
2940 let empty_buffer = executor.create_empty_buffer(empty_schema).unwrap();
2941 executor.store_mut().put("empty_rel", empty_buffer);
2942 executor.register_relation(RelId(4), "empty_rel");
2943
2944 let base = Box::new(RirNode::Scan { rel: RelId(1) });
2947 let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
2948
2949 let node = RirNode::Fixpoint {
2950 scc_id: 0,
2951 base,
2952 recursive,
2953 delta_rel: RelId(2),
2954 full_rel: RelId(3),
2955 };
2956
2957 let result = executor.execute_node(&node);
2958 assert!(result.is_ok());
2959
2960 let result = result.unwrap();
2962 assert_eq!(buffer_row_count(&executor, &result), 3);
2963 let values = read_buffer_u32(&executor, &result, 0);
2964 assert_eq!(values, vec![1, 2, 3]);
2965 }
2966
2967 #[test]
2968 fn test_execute_fixpoint_empty_base() {
2969 let mut executor = match create_test_executor() {
2971 Some(e) => e,
2972 None => {
2973 eprintln!("Skipping test: no CUDA device available");
2974 return;
2975 }
2976 };
2977
2978 let empty_schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
2980 let empty_buffer = executor.create_empty_buffer(empty_schema.clone()).unwrap();
2981 executor.store_mut().put("empty_base", empty_buffer);
2982 executor.register_relation(RelId(1), "empty_base");
2983
2984 let rec_buffer = create_test_buffer(&executor, &[4, 5, 6], "key");
2986 executor.store_mut().put("rec_rel", rec_buffer);
2987 executor.register_relation(RelId(4), "rec_rel");
2988
2989 let base = Box::new(RirNode::Scan { rel: RelId(1) });
2990 let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
2991
2992 let node = RirNode::Fixpoint {
2993 scc_id: 0,
2994 base,
2995 recursive,
2996 delta_rel: RelId(2),
2997 full_rel: RelId(3),
2998 };
2999
3000 let result = executor.execute_node(&node);
3001 assert!(result.is_ok());
3002
3003 let result = result.unwrap();
3005 assert_eq!(buffer_row_count(&executor, &result), 0);
3006 }
3007
3008 #[test]
3009 fn test_execute_fixpoint_one_iteration() {
3010 let mut executor = match create_test_executor() {
3012 Some(e) => e,
3013 None => {
3014 eprintln!("Skipping test: no CUDA device available");
3015 return;
3016 }
3017 };
3018
3019 let base_buffer = create_test_buffer(&executor, &[1, 2], "key");
3021 executor.store_mut().put("base_rel", base_buffer);
3022 executor.register_relation(RelId(1), "base_rel");
3023
3024 let rec_buffer = create_test_buffer(&executor, &[1, 2, 3], "key");
3026 executor.store_mut().put("rec_rel", rec_buffer);
3027 executor.register_relation(RelId(4), "rec_rel");
3028
3029 let base = Box::new(RirNode::Scan { rel: RelId(1) });
3033 let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
3034
3035 let node = RirNode::Fixpoint {
3036 scc_id: 0,
3037 base,
3038 recursive,
3039 delta_rel: RelId(2),
3040 full_rel: RelId(3),
3041 };
3042
3043 let result = executor.execute_node(&node);
3044 assert!(result.is_ok());
3045
3046 let result = result.unwrap();
3047 assert_eq!(buffer_row_count(&executor, &result), 3);
3049 }
3050
3051 #[test]
3052 fn test_execute_fixpoint_multiple_iterations() {
3053 let mut executor = match create_test_executor() {
3056 Some(e) => e,
3057 None => {
3058 eprintln!("Skipping test: no CUDA device available");
3059 return;
3060 }
3061 };
3062
3063 let base_buffer = create_test_buffer(&executor, &[1], "key");
3065 executor.store_mut().put("base_rel", base_buffer);
3066 executor.register_relation(RelId(1), "base_rel");
3067
3068 let rec_buffer = create_test_buffer(&executor, &[1, 2], "key");
3080 executor.store_mut().put("rec_rel", rec_buffer);
3081 executor.register_relation(RelId(4), "rec_rel");
3082
3083 let base = Box::new(RirNode::Scan { rel: RelId(1) });
3084 let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
3085
3086 let node = RirNode::Fixpoint {
3087 scc_id: 0,
3088 base,
3089 recursive,
3090 delta_rel: RelId(2),
3091 full_rel: RelId(3),
3092 };
3093
3094 let result = executor.execute_node(&node);
3095 assert!(result.is_ok());
3096
3097 let result = result.unwrap();
3098 assert_eq!(buffer_row_count(&executor, &result), 2);
3100 }
3101
3102 #[test]
3103 fn test_execute_fixpoint_honors_runtime_iteration_limit() {
3104 let mut config = RuntimeConfig::default();
3105 config.max_iterations = 1;
3106 let mut executor = match create_test_executor_with_config(config) {
3107 Some(executor) => executor,
3108 None => {
3109 eprintln!("Skipping test: no CUDA device available");
3110 return;
3111 }
3112 };
3113
3114 let base_buffer = create_test_buffer(&executor, &[1], "key");
3115 executor.store_mut().put("base_rel", base_buffer);
3116 executor.register_relation(RelId(1), "base_rel");
3117
3118 let recursive_buffer = create_test_buffer(&executor, &[1, 2], "key");
3119 executor.store_mut().put("recursive_rel", recursive_buffer);
3120 executor.register_relation(RelId(4), "recursive_rel");
3121
3122 let node = RirNode::Fixpoint {
3123 scc_id: 7,
3124 base: Box::new(RirNode::Scan { rel: RelId(1) }),
3125 recursive: Box::new(RirNode::Scan { rel: RelId(4) }),
3126 delta_rel: RelId(2),
3127 full_rel: RelId(3),
3128 };
3129
3130 let error = match executor.execute_node(&node) {
3131 Ok(_) => panic!("fixpoint unexpectedly ignored max_iterations=1"),
3132 Err(error) => error,
3133 };
3134 assert!(
3135 error
3136 .to_string()
3137 .contains("Fixpoint iteration limit (1) exceeded for SCC 7"),
3138 "unexpected error: {error}"
3139 );
3140 }
3141
3142 #[test]
3143 fn test_execute_fixpoint_via_node() {
3144 let mut executor = match create_test_executor() {
3146 Some(e) => e,
3147 None => {
3148 eprintln!("Skipping test: no CUDA device available");
3149 return;
3150 }
3151 };
3152
3153 let buffer = create_test_buffer(&executor, &[1, 2, 3], "key");
3155 executor.store_mut().put("base_rel", buffer);
3156 executor.register_relation(RelId(1), "base_rel");
3157
3158 let empty_schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
3160 let empty_buffer = executor.create_empty_buffer(empty_schema).unwrap();
3161 executor.store_mut().put("empty_rel", empty_buffer);
3162 executor.register_relation(RelId(4), "empty_rel");
3163
3164 let base = Box::new(RirNode::Scan { rel: RelId(1) });
3165 let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
3166
3167 let node = RirNode::Fixpoint {
3168 scc_id: 0,
3169 base,
3170 recursive,
3171 delta_rel: RelId(2),
3172 full_rel: RelId(3),
3173 };
3174
3175 let result = executor.execute_node(&node);
3176 assert!(result.is_ok());
3177
3178 let result = result.unwrap();
3179 assert_eq!(buffer_row_count(&executor, &result), 3);
3180 }
3181
3182 #[test]
3183 fn test_fixpoint_cleanup() {
3184 let mut executor = match create_test_executor() {
3186 Some(e) => e,
3187 None => {
3188 eprintln!("Skipping test: no CUDA device available");
3189 return;
3190 }
3191 };
3192
3193 let buffer = create_test_buffer(&executor, &[1, 2], "key");
3194 executor.store_mut().put("base_rel", buffer);
3195 executor.register_relation(RelId(1), "base_rel");
3196
3197 let empty_schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
3198 let empty_buffer = executor.create_empty_buffer(empty_schema).unwrap();
3199 executor.store_mut().put("empty_rel", empty_buffer);
3200 executor.register_relation(RelId(4), "empty_rel");
3201
3202 executor.register_relation(RelId(2), "__delta_test");
3204 executor.register_relation(RelId(3), "__full_test");
3205
3206 let base = Box::new(RirNode::Scan { rel: RelId(1) });
3207 let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
3208
3209 let node = RirNode::Fixpoint {
3210 scc_id: 0,
3211 base,
3212 recursive,
3213 delta_rel: RelId(2),
3214 full_rel: RelId(3),
3215 };
3216
3217 let result = executor.execute_node(&node);
3218 assert!(result.is_ok());
3219
3220 assert!(!executor.store().contains("__delta_test"));
3222 assert!(!executor.store().contains("__full_test"));
3223 }
3224
3225 #[test]
3228 fn test_execute_plan_empty() {
3229 let mut executor = match create_test_executor() {
3230 Some(e) => e,
3231 None => {
3232 eprintln!("Skipping test: no CUDA device available");
3233 return;
3234 }
3235 };
3236
3237 let plan = ExecutionPlan::new(vec![]);
3238
3239 let result = executor.execute_plan(&plan);
3240 assert!(result.is_ok());
3241 let result = result.unwrap();
3242 assert_eq!(buffer_row_count(&executor, &result), 0);
3243 }
3244
3245 #[test]
3246 fn test_execute_plan_with_stratum() {
3247 let mut executor = match create_test_executor() {
3248 Some(e) => e,
3249 None => {
3250 eprintln!("Skipping test: no CUDA device available");
3251 return;
3252 }
3253 };
3254
3255 let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
3257 executor.store_mut().put("input", buffer);
3258 executor.register_relation(RelId(1), "input");
3259
3260 let scc = Scc {
3262 id: 0,
3263 predicates: vec!["output".to_string()],
3264 is_recursive: false,
3265 };
3266
3267 let rule = CompiledRule {
3268 head: "output".to_string(),
3269 body: RirNode::Scan { rel: RelId(1) },
3270 meta: RirMeta::default(),
3271 };
3272
3273 let stratum = Stratum {
3274 id: 0,
3275 sccs: vec![0],
3276 };
3277
3278 let plan = ExecutionPlan {
3279 sccs: vec![scc],
3280 strata: vec![stratum],
3281 rules_by_scc: vec![vec![rule]],
3282 generated_query_rules: vec![],
3283 est_memory_peak: 0,
3284 rel_arities: std::collections::HashMap::new(),
3285 };
3286
3287 let result = executor.execute_plan(&plan);
3288 assert!(result.is_ok());
3289
3290 assert!(executor.store().contains("output"));
3292 let output = executor.store().get("output").unwrap();
3293 assert_eq!(buffer_row_count(&executor, output), 5);
3294 }
3295
3296 #[test]
3297 fn test_apply_deltas_and_recompute_updates_dependents() {
3298 let mut executor = match create_test_executor() {
3299 Some(e) => e,
3300 None => {
3301 eprintln!("Skipping test: no CUDA device available");
3302 return;
3303 }
3304 };
3305
3306 let input = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
3307 executor.store_mut().put("input", input);
3308 executor.register_relation(RelId(1), "input");
3309
3310 let scc0 = Scc {
3313 id: 0,
3314 predicates: vec!["input".to_string()],
3315 is_recursive: false,
3316 };
3317 let scc1 = Scc {
3318 id: 1,
3319 predicates: vec!["output".to_string()],
3320 is_recursive: false,
3321 };
3322
3323 let input_rule = CompiledRule {
3324 head: "input".to_string(),
3325 body: RirNode::Scan { rel: RelId(1) },
3326 meta: RirMeta::default(),
3327 };
3328
3329 let output_rule = CompiledRule {
3330 head: "output".to_string(),
3331 body: RirNode::Filter {
3332 input: Box::new(RirNode::Scan { rel: RelId(1) }),
3333 predicate: Expr::Compare {
3334 left: Box::new(Expr::Column(0)),
3335 op: CompareOp::Gt,
3336 right: Box::new(Expr::Const(ConstValue::U32(2))),
3337 },
3338 },
3339 meta: RirMeta::default(),
3340 };
3341
3342 let stratum = Stratum {
3343 id: 0,
3344 sccs: vec![0, 1],
3345 };
3346
3347 let plan = ExecutionPlan {
3348 sccs: vec![scc0, scc1],
3349 strata: vec![stratum],
3350 rules_by_scc: vec![vec![input_rule], vec![output_rule]],
3351 generated_query_rules: vec![],
3352 est_memory_peak: 0,
3353 rel_arities: std::collections::HashMap::new(),
3354 };
3355
3356 executor.execute_plan(&plan).expect("initial execute_plan");
3357 let initial_out = executor.store().get("output").expect("output missing");
3358 let initial_vals = read_buffer_u32(&executor, initial_out, 0);
3359 assert_eq!(initial_vals, vec![3, 4, 5]);
3360
3361 let delete_buf = create_test_buffer(&executor, &[5], "key");
3362 let insert_buf = create_test_buffer(&executor, &[10], "key");
3363
3364 let mut deltas = HashMap::new();
3365 deltas.insert(
3366 "input".to_string(),
3367 RelationDelta::new(Some(insert_buf), Some(delete_buf)),
3368 );
3369
3370 executor
3371 .apply_deltas_and_recompute(&plan, &deltas)
3372 .expect("apply_deltas_and_recompute");
3373
3374 let out = executor
3375 .store()
3376 .get("output")
3377 .expect("output missing after recompute");
3378 let vals = read_buffer_u32(&executor, out, 0);
3379 assert_eq!(vals, vec![3, 4, 10]);
3380 }
3381
3382 #[test]
3383 fn test_apply_deltas_and_recompute_insert_only_recomputes_anti_join() {
3384 let mut executor = match create_test_executor() {
3385 Some(e) => e,
3386 None => {
3387 eprintln!("Skipping test: no CUDA device available");
3388 return;
3389 }
3390 };
3391
3392 let lhs = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
3393 executor.store_mut().put("lhs", lhs);
3394 executor.register_relation(RelId(1), "lhs");
3395
3396 let blocked = create_test_buffer(&executor, &[], "key");
3397 executor.store_mut().put("blocked", blocked);
3398 executor.register_relation(RelId(2), "blocked");
3399
3400 let scc0 = Scc {
3404 id: 0,
3405 predicates: vec!["lhs".to_string()],
3406 is_recursive: false,
3407 };
3408 let scc1 = Scc {
3409 id: 1,
3410 predicates: vec!["blocked".to_string()],
3411 is_recursive: false,
3412 };
3413 let scc2 = Scc {
3414 id: 2,
3415 predicates: vec!["out".to_string()],
3416 is_recursive: false,
3417 };
3418
3419 let lhs_rule = CompiledRule {
3420 head: "lhs".to_string(),
3421 body: RirNode::Scan { rel: RelId(1) },
3422 meta: RirMeta::default(),
3423 };
3424 let blocked_rule = CompiledRule {
3425 head: "blocked".to_string(),
3426 body: RirNode::Scan { rel: RelId(2) },
3427 meta: RirMeta::default(),
3428 };
3429 let out_rule = CompiledRule {
3430 head: "out".to_string(),
3431 body: RirNode::Join {
3432 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3433 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3434 left_keys: vec![0],
3435 right_keys: vec![0],
3436 join_type: JoinType::Anti,
3437 },
3438 meta: RirMeta::default(),
3439 };
3440
3441 let stratum = Stratum {
3442 id: 0,
3443 sccs: vec![0, 1, 2],
3444 };
3445
3446 let plan = ExecutionPlan {
3447 sccs: vec![scc0, scc1, scc2],
3448 strata: vec![stratum],
3449 rules_by_scc: vec![vec![lhs_rule], vec![blocked_rule], vec![out_rule]],
3450 generated_query_rules: vec![],
3451 est_memory_peak: 0,
3452 rel_arities: std::collections::HashMap::new(),
3453 };
3454
3455 executor.execute_plan(&plan).expect("initial execute_plan");
3456 let initial = executor.store().get("out").expect("out missing");
3457 let initial_vals = read_buffer_u32(&executor, initial, 0);
3458 assert_eq!(initial_vals, vec![1, 2, 3, 4, 5]);
3459
3460 let insert_buf = create_test_buffer(&executor, &[2, 4], "key");
3462 let mut deltas = HashMap::new();
3463 deltas.insert(
3464 "blocked".to_string(),
3465 RelationDelta::new(Some(insert_buf), None),
3466 );
3467
3468 executor
3469 .apply_deltas_and_recompute(&plan, &deltas)
3470 .expect("apply_deltas_and_recompute");
3471
3472 let out = executor
3473 .store()
3474 .get("out")
3475 .expect("out missing after update");
3476 let vals = read_buffer_u32(&executor, out, 0);
3477 assert_eq!(vals, vec![1, 3, 5]);
3478 }
3479
3480 #[test]
3483 fn test_execute_filter_project_chain() {
3484 let mut executor = match create_test_executor() {
3485 Some(e) => e,
3486 None => {
3487 eprintln!("Skipping test: no CUDA device available");
3488 return;
3489 }
3490 };
3491
3492 let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
3494 executor.store_mut().put("input", buffer);
3495 executor.register_relation(RelId(1), "input");
3496
3497 let scan = RirNode::Scan { rel: RelId(1) };
3499 let filter = RirNode::Filter {
3500 input: Box::new(scan),
3501 predicate: Expr::Compare {
3502 left: Box::new(Expr::Column(0)),
3503 op: CompareOp::Gt,
3504 right: Box::new(Expr::Const(ConstValue::U32(2))),
3505 },
3506 };
3507 let project = RirNode::Project {
3508 input: Box::new(filter),
3509 columns: vec![ProjectExpr::Column(0)],
3510 };
3511
3512 let result = executor.execute_node(&project);
3513 assert!(result.is_ok());
3514
3515 let result = result.unwrap();
3516 assert_eq!(buffer_row_count(&executor, &result), 3);
3517
3518 let values = read_buffer_u32(&executor, &result, 0);
3519 assert_eq!(values, vec![3, 4, 5]);
3520 }
3521
3522 #[test]
3523 fn test_project_profiling_observes_input_and_output_allocations() {
3524 let mut executor = match create_test_executor_with_config(
3525 RuntimeConfig::default().with_common_subexpression_elimination(Some(false)),
3526 ) {
3527 Some(executor) => executor,
3528 None => {
3529 eprintln!("Skipping test: no CUDA device available");
3530 return;
3531 }
3532 };
3533 let buffer = create_test_buffer(&executor, &[1, 2, 3, 4], "key");
3534 executor.store_mut().put("input", buffer);
3535 executor.register_relation(RelId(1), "input");
3536 let columns = vec![ProjectExpr::Column(0)];
3537
3538 let baseline = executor.provider.memory().allocated_bytes();
3539 let input = executor.execute_scan(RelId(1)).expect("clone scan input");
3540 let output = executor
3541 .execute_project(&input, &columns)
3542 .expect("project while retaining input");
3543 let expected_observation = executor.provider.memory().allocated_bytes();
3544 drop(output);
3545 drop(input);
3546 assert_eq!(executor.provider.memory().allocated_bytes(), baseline);
3547
3548 executor.set_profiling(true);
3549 let node = RirNode::Project {
3550 input: Box::new(RirNode::Scan { rel: RelId(1) }),
3551 columns,
3552 };
3553 let _result = executor
3554 .execute_node(&node)
3555 .expect("execute profiled project");
3556 let observed_peak = executor.execution_stats(0).peak_memory_bytes;
3557
3558 assert!(
3559 observed_peak >= expected_observation,
3560 "profiled project peak {observed_peak} omitted live input allocation; expected at least {expected_observation}"
3561 );
3562 }
3563
3564 fn duplicate_join_union_plan() -> RirNode {
3567 let join = RirNode::Join {
3568 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3569 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3570 left_keys: vec![0],
3571 right_keys: vec![0],
3572 join_type: JoinType::Inner,
3573 };
3574 RirNode::Union {
3575 inputs: vec![join.clone(), join],
3576 }
3577 }
3578
3579 fn seed_cse_join_fixture(executor: &mut Executor, right: &[u32]) {
3580 executor.register_relation(RelId(1), "left");
3581 executor.register_relation(RelId(2), "right");
3582 let left = create_test_buffer(executor, &[1, 2, 3, 4], "key");
3583 let right = create_test_buffer(executor, right, "key");
3584 executor.put_relation("left", left);
3585 executor.put_relation("right", right);
3586 }
3587
3588 #[test]
3589 fn test_common_subexpression_cache_reuses_duplicate_inner_join_when_enabled() {
3590 let mut executor = match create_test_executor_with_config(
3591 RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3592 ) {
3593 Some(e) => e,
3594 None => {
3595 eprintln!("Skipping test: no CUDA device available");
3596 return;
3597 }
3598 };
3599 seed_cse_join_fixture(&mut executor, &[2, 3, 5]);
3600
3601 let result = executor
3602 .execute_node(&duplicate_join_union_plan())
3603 .expect("duplicate join union executes");
3604
3605 assert_eq!(buffer_row_count(&executor, &result), 2);
3606 let stats = executor.common_subexpression_stats();
3607 assert_eq!(stats.hits, 1);
3608 assert!(stats.misses >= 1);
3609 assert_eq!(stats.unsafe_rejections, 0);
3610 }
3611
3612 #[test]
3613 fn test_common_subexpression_cache_treats_project_chains_atomically() {
3614 let mut executor = match create_test_executor_with_config(
3615 RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3616 ) {
3617 Some(executor) => executor,
3618 None => {
3619 eprintln!("Skipping test: no CUDA device available");
3620 return;
3621 }
3622 };
3623 executor.register_relation(RelId(1), "input");
3624 let input = create_test_buffer(&executor, &[1, 2, 3], "key");
3625 executor.put_relation("input", input);
3626
3627 let inner = RirNode::Project {
3628 input: Box::new(RirNode::Scan { rel: RelId(1) }),
3629 columns: vec![ProjectExpr::Column(0)],
3630 };
3631 let passthrough = RirNode::Project {
3632 input: Box::new(inner.clone()),
3633 columns: vec![ProjectExpr::Column(0)],
3634 };
3635 let computed = RirNode::Project {
3636 input: Box::new(inner),
3637 columns: vec![ProjectExpr::Computed(
3638 Expr::Add(
3639 Box::new(Expr::Column(0)),
3640 Box::new(Expr::Const(ConstValue::U32(1))),
3641 ),
3642 ScalarType::U32,
3643 )],
3644 };
3645
3646 executor
3647 .execute_node(&passthrough)
3648 .expect("first project chain executes");
3649 let result = executor
3650 .execute_node(&computed)
3651 .expect("second project chain executes");
3652
3653 assert_eq!(read_buffer_u32(&executor, &result, 0), vec![2, 3, 4]);
3654 executor
3655 .execute_node(&computed)
3656 .expect("duplicate full project chain executes");
3657 let stats = executor.common_subexpression_stats();
3658 assert_eq!(stats.hits, 1);
3659 assert_eq!(stats.misses, 2);
3660 }
3661
3662 #[test]
3663 fn test_common_subexpression_off_on_preserves_output_and_records_reuse_only_when_enabled() {
3664 let mut disabled = match create_test_executor_with_config(
3665 RuntimeConfig::default().with_common_subexpression_elimination(Some(false)),
3666 ) {
3667 Some(e) => e,
3668 None => {
3669 eprintln!("Skipping test: no CUDA device available");
3670 return;
3671 }
3672 };
3673 let mut enabled = match create_test_executor_with_config(
3674 RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3675 ) {
3676 Some(e) => e,
3677 None => {
3678 eprintln!("Skipping test: no CUDA device available");
3679 return;
3680 }
3681 };
3682 seed_cse_join_fixture(&mut disabled, &[2, 3, 5]);
3683 seed_cse_join_fixture(&mut enabled, &[2, 3, 5]);
3684 let plan = duplicate_join_union_plan();
3685
3686 disabled.provider.reset_d2h_transfer_count();
3687 enabled.provider.reset_d2h_transfer_count();
3688 let disabled_result = disabled.execute_node(&plan).expect("disabled CSE output");
3689 let enabled_result = enabled.execute_node(&plan).expect("enabled CSE output");
3690 let disabled_d2h = disabled.provider.d2h_transfer_count();
3691 let enabled_d2h = enabled.provider.d2h_transfer_count();
3692
3693 assert_eq!(
3694 read_buffer_u32(&disabled, &disabled_result, 0),
3695 read_buffer_u32(&enabled, &enabled_result, 0)
3696 );
3697 assert_eq!(enabled_d2h, disabled_d2h);
3698 assert_eq!(disabled.common_subexpression_stats().hits, 0);
3699 assert_eq!(disabled.common_subexpression_stats().misses, 0);
3700 assert_eq!(enabled.common_subexpression_stats().hits, 1);
3701 }
3702
3703 #[test]
3704 fn test_common_subexpression_cache_invalidates_on_relation_generation_change() {
3705 let mut executor = match create_test_executor_with_config(
3706 RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3707 ) {
3708 Some(e) => e,
3709 None => {
3710 eprintln!("Skipping test: no CUDA device available");
3711 return;
3712 }
3713 };
3714 seed_cse_join_fixture(&mut executor, &[2, 3, 5]);
3715 let plan = duplicate_join_union_plan();
3716
3717 executor.execute_node(&plan).expect("first execution");
3718 assert_eq!(executor.common_subexpression_stats().hits, 1);
3719
3720 let changed_right = create_test_buffer(&executor, &[4], "key");
3721 executor.put_relation("right", changed_right);
3722 let result = executor.execute_node(&plan).expect("second execution");
3723
3724 assert_eq!(buffer_row_count(&executor, &result), 1);
3725 let stats = executor.common_subexpression_stats();
3726 assert_eq!(stats.hits, 2);
3727 assert!(stats.misses >= 2);
3728 }
3729
3730 #[test]
3731 fn test_common_subexpression_cache_rejects_unsafe_difference_boundary() {
3732 let mut executor = match create_test_executor_with_config(
3733 RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3734 ) {
3735 Some(e) => e,
3736 None => {
3737 eprintln!("Skipping test: no CUDA device available");
3738 return;
3739 }
3740 };
3741 seed_cse_join_fixture(&mut executor, &[2, 3, 5]);
3742 let diff = RirNode::Diff {
3743 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3744 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3745 };
3746
3747 executor
3748 .execute_node(&RirNode::Union {
3749 inputs: vec![diff.clone(), diff],
3750 })
3751 .expect("unsafe duplicate diff still executes without CSE sharing");
3752
3753 let stats = executor.common_subexpression_stats();
3754 assert_eq!(stats.hits, 0);
3755 assert!(stats.unsafe_rejections >= 1);
3756 assert!(stats
3757 .rejection_reasons
3758 .iter()
3759 .any(|reason| reason == "negation_or_difference_boundary"));
3760 }
3761
3762 #[test]
3763 fn test_common_subexpression_key_rejects_aggregate_and_tensor_boundaries() {
3764 let mut executor = match create_test_executor_with_config(
3765 RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3766 ) {
3767 Some(e) => e,
3768 None => {
3769 eprintln!("Skipping test: no CUDA device available");
3770 return;
3771 }
3772 };
3773 let aggregate = RirNode::GroupBy {
3774 input: Box::new(RirNode::Scan { rel: RelId(1) }),
3775 key_cols: vec![0],
3776 aggs: vec![(0, xlog_core::AggOp::Count)],
3777 };
3778 let tensor = RirNode::TensorMaskedJoin {
3779 mask_name: "W".to_string(),
3780 schema_size: 1,
3781 left_keys: vec![0],
3782 right_keys: vec![0],
3783 rel_index: vec![(RelId(1), "left".to_string())],
3784 head_rel_name: "head".to_string(),
3785 head_rel_id: RelId(3),
3786 max_active_rules: 1,
3787 head_projection: vec![0],
3788 };
3789 let chain = RirNode::ChainJoin {
3790 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3791 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3792 left_key: 0,
3793 right_key: 0,
3794 output_columns: vec![ProjectExpr::Column(0)],
3795 fallback: Box::new(RirNode::Join {
3796 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3797 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3798 left_keys: vec![0],
3799 right_keys: vec![0],
3800 join_type: JoinType::Inner,
3801 }),
3802 };
3803
3804 assert!(executor.common_subexpression_key(&aggregate).is_none());
3805 assert!(executor.common_subexpression_key(&tensor).is_none());
3806 assert!(executor.common_subexpression_key(&chain).is_none());
3807
3808 let reasons = &executor.common_subexpression_stats().rejection_reasons;
3809 assert!(reasons.iter().any(|reason| reason == "aggregate_boundary"));
3810 assert!(reasons
3811 .iter()
3812 .any(|reason| reason == "provenance_or_tensor_boundary"));
3813 assert!(reasons
3814 .iter()
3815 .any(|reason| reason == "specialized_dispatch_boundary"));
3816 }
3817
3818 fn adaptive_scc() -> Scc {
3821 Scc {
3822 id: 0,
3823 predicates: vec!["out".to_string()],
3824 is_recursive: false,
3825 }
3826 }
3827
3828 fn adaptive_stratum() -> Stratum {
3829 Stratum {
3830 id: 0,
3831 sccs: vec![0],
3832 }
3833 }
3834
3835 fn adaptive_rule(body: RirNode) -> CompiledRule {
3836 CompiledRule {
3837 head: "out".to_string(),
3838 body,
3839 meta: RirMeta::default(),
3840 }
3841 }
3842
3843 fn adaptive_plan(body: RirNode) -> ExecutionPlan {
3844 ExecutionPlan {
3845 sccs: vec![adaptive_scc()],
3846 strata: vec![adaptive_stratum()],
3847 rules_by_scc: vec![vec![adaptive_rule(body)]],
3848 generated_query_rules: vec![],
3849 est_memory_peak: 0,
3850 rel_arities: std::collections::HashMap::new(),
3851 }
3852 }
3853
3854 fn adaptive_baseline_join_plan() -> ExecutionPlan {
3855 adaptive_plan(RirNode::Project {
3856 input: Box::new(RirNode::Join {
3857 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3858 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3859 left_keys: vec![0],
3860 right_keys: vec![0],
3861 join_type: JoinType::Inner,
3862 }),
3863 columns: vec![ProjectExpr::Column(0)],
3864 })
3865 }
3866
3867 fn adaptive_scan_candidate_plan(rel: RelId) -> ExecutionPlan {
3868 adaptive_plan(RirNode::Scan { rel })
3869 }
3870
3871 fn seed_adaptive_fixture(executor: &mut Executor, right: &[u32]) {
3872 executor.register_relation(RelId(1), "left");
3873 executor.register_relation(RelId(2), "right");
3874 let left = create_test_buffer(executor, &[1, 2, 3, 4, 5, 6, 7, 8], "key");
3875 let right = create_test_buffer(executor, right, "key");
3876 executor.put_relation("left", left);
3877 executor.put_relation("right", right);
3878 }
3879
3880 #[test]
3881 fn test_adaptive_reoptimization_disabled_uses_baseline_and_records_decision() {
3882 let mut executor = match create_test_executor_with_config(
3883 RuntimeConfig::default().with_adaptive_reoptimization(Some(false)),
3884 ) {
3885 Some(e) => e,
3886 None => {
3887 eprintln!("Skipping test: no CUDA device available");
3888 return;
3889 }
3890 };
3891 seed_adaptive_fixture(&mut executor, &[1, 2, 3, 4, 5, 6, 7, 8]);
3892
3893 let baseline = adaptive_baseline_join_plan();
3894 let candidate = adaptive_scan_candidate_plan(RelId(2));
3895 let result = executor
3896 .execute_plan_with_adaptive_candidate(&baseline, &candidate)
3897 .expect("disabled adaptation executes baseline");
3898
3899 assert_eq!(
3900 read_buffer_u32(&executor, &result, 0),
3901 (1..=8).collect::<Vec<_>>()
3902 );
3903 let stats = executor.adaptive_reoptimization_stats();
3904 assert_eq!(stats.disabled, 1);
3905 assert_eq!(stats.adopted, 0);
3906 assert_eq!(stats.rolled_back, 0);
3907 assert_eq!(
3908 stats.last_decision.as_ref().map(|decision| decision.action),
3909 Some(AdaptiveReoptimizationAction::Disabled)
3910 );
3911 }
3912
3913 #[test]
3914 fn test_adaptive_reoptimization_adopts_equivalent_candidate_and_records_telemetry() {
3915 let mut executor = match create_test_executor_with_config(
3916 RuntimeConfig::default().with_adaptive_reoptimization(Some(true)),
3917 ) {
3918 Some(e) => e,
3919 None => {
3920 eprintln!("Skipping test: no CUDA device available");
3921 return;
3922 }
3923 };
3924 seed_adaptive_fixture(&mut executor, &[1, 2, 3, 4, 5, 6, 7, 8]);
3925 executor.provider.reset_host_transfer_stats();
3926
3927 let baseline = adaptive_baseline_join_plan();
3928 let candidate = adaptive_scan_candidate_plan(RelId(1));
3929 let result = executor
3930 .execute_plan_with_adaptive_candidate(&baseline, &candidate)
3931 .expect("equivalent candidate is adopted");
3932
3933 assert_eq!(
3934 read_buffer_u32(&executor, &result, 0),
3935 (1..=8).collect::<Vec<_>>()
3936 );
3937 let stats = executor.adaptive_reoptimization_stats();
3938 assert_eq!(stats.adopted, 1);
3939 assert_eq!(stats.rolled_back, 0);
3940 assert_eq!(stats.last_observations.len(), 1);
3941 assert!(stats.last_observations[0].cardinality_delta_abs > 0);
3942 assert!(stats.last_observations[0].selectivity_delta_abs > 0.0);
3943 assert_eq!(stats.data_plane_dtoh_calls, 0);
3944 }
3945
3946 #[test]
3947 fn test_adaptive_reoptimization_rolls_back_bad_candidate_with_typed_diagnostic() {
3948 let mut executor = match create_test_executor_with_config(
3949 RuntimeConfig::default().with_adaptive_reoptimization(Some(true)),
3950 ) {
3951 Some(e) => e,
3952 None => {
3953 eprintln!("Skipping test: no CUDA device available");
3954 return;
3955 }
3956 };
3957 seed_adaptive_fixture(&mut executor, &[1, 2, 3, 4, 5, 6, 7, 8]);
3958
3959 let baseline = adaptive_baseline_join_plan();
3960 let bad_candidate = adaptive_scan_candidate_plan(RelId(2));
3961 executor.put_relation("right", create_test_buffer(&executor, &[99], "key"));
3962 let result = executor
3963 .execute_plan_with_adaptive_candidate(&baseline, &bad_candidate)
3964 .expect("bad candidate rolls back to baseline output");
3965
3966 assert_eq!(read_buffer_u32(&executor, &result, 0), Vec::<u32>::new());
3967 let out = executor.store().get("out").expect("rollback restored out");
3968 assert_eq!(read_buffer_u32(&executor, out, 0), Vec::<u32>::new());
3969 let stats = executor.adaptive_reoptimization_stats();
3970 assert_eq!(stats.adopted, 0);
3971 assert_eq!(stats.rolled_back, 1);
3972 assert!(stats.diagnostics.iter().any(|diagnostic| {
3973 diagnostic.kind == AdaptiveReoptimizationDiagnosticKind::CandidateOutputMismatch
3974 }));
3975 }
3976
3977 #[test]
3978 fn test_adaptive_reoptimization_decisions_are_deterministic_under_replay() {
3979 let mut executor = match create_test_executor_with_config(
3980 RuntimeConfig::default().with_adaptive_reoptimization(Some(true)),
3981 ) {
3982 Some(e) => e,
3983 None => {
3984 eprintln!("Skipping test: no CUDA device available");
3985 return;
3986 }
3987 };
3988 seed_adaptive_fixture(&mut executor, &[1, 2, 3, 4, 5, 6, 7, 8]);
3989 let baseline = adaptive_baseline_join_plan();
3990 executor
3991 .execute_plan(&baseline)
3992 .expect("baseline execution records telemetry");
3993 let observations = executor
3994 .adaptive_reoptimization_stats()
3995 .last_observations
3996 .clone();
3997
3998 let first = executor
3999 .replay_adaptive_reoptimization_decision(&observations)
4000 .unwrap();
4001 for _ in 0..100 {
4002 assert_eq!(
4003 executor
4004 .replay_adaptive_reoptimization_decision(&observations)
4005 .unwrap(),
4006 first
4007 );
4008 }
4009 }
4010
4011 fn persistent_index_join_plan() -> ExecutionPlan {
4014 adaptive_baseline_join_plan()
4015 }
4016
4017 fn persistent_index_heavy_join_plan(repetitions: usize) -> ExecutionPlan {
4018 let mut inputs = Vec::with_capacity(repetitions);
4019 for _ in 0..repetitions {
4020 inputs.push(RirNode::Project {
4021 input: Box::new(RirNode::Join {
4022 left: Box::new(RirNode::Scan { rel: RelId(1) }),
4023 right: Box::new(RirNode::Scan { rel: RelId(2) }),
4024 left_keys: vec![0],
4025 right_keys: vec![0],
4026 join_type: JoinType::Semi,
4027 }),
4028 columns: vec![ProjectExpr::Column(0)],
4029 });
4030 }
4031 adaptive_plan(RirNode::Union { inputs })
4032 }
4033
4034 fn seed_persistent_index_fixture(executor: &mut Executor, rows: u32) {
4035 executor.register_relation(RelId(1), "left");
4036 executor.register_relation(RelId(2), "right");
4037 let values: Vec<u32> = (0..rows).collect();
4038 let left = create_test_buffer(executor, &values, "key");
4039 let right = create_test_buffer(executor, &values, "key");
4040 executor.put_relation("left", left);
4041 executor.put_relation("right", right);
4042 }
4043
4044 fn seed_persistent_index_performance_fixture(
4045 executor: &mut Executor,
4046 left_rows: u32,
4047 right_rows: u32,
4048 ) {
4049 executor.register_relation(RelId(1), "left");
4050 executor.register_relation(RelId(2), "right");
4051 let left_values: Vec<u32> = (0..left_rows).collect();
4052 let right_values: Vec<u32> = (0..right_rows).collect();
4053 let left = create_test_buffer(executor, &left_values, "key");
4054 let right = create_test_buffer(executor, &right_values, "key");
4055 executor.put_relation("left", left);
4056 executor.put_relation("right", right);
4057 }
4058
4059 fn warm_persistent_index(executor: &mut Executor, plan: &ExecutionPlan, times: usize) {
4060 for _ in 0..times {
4061 executor.execute_plan(plan).expect("persistent index plan");
4062 }
4063 }
4064
4065 fn median_duration(samples: &mut [Duration]) -> Duration {
4066 samples.sort_unstable();
4067 samples[samples.len() / 2]
4068 }
4069
4070 fn measure_persistent_index_fixture(
4071 mut executor: Executor,
4072 plan: &ExecutionPlan,
4073 warmup: usize,
4074 iterations: usize,
4075 ) -> (
4076 Duration,
4077 u64,
4078 JoinIndexCacheStats,
4079 xlog_cuda::provider::HostTransferStats,
4080 ) {
4081 let mut output_rows = None;
4082 warm_persistent_index(&mut executor, plan, warmup);
4083 executor.provider.reset_host_transfer_stats();
4084
4085 let mut samples = Vec::with_capacity(iterations);
4086 for _ in 0..iterations {
4087 let start = Instant::now();
4088 let output = executor.execute_plan(plan).expect("persistent index plan");
4089 executor
4090 .provider
4091 .device()
4092 .synchronize()
4093 .expect("sync device");
4094 samples.push(start.elapsed());
4095 output_rows = Some(if let Some(buffer) = executor.store().get("out") {
4096 executor
4097 .buffer_row_count(buffer)
4098 .expect("read output row count")
4099 .into()
4100 } else {
4101 output.num_rows()
4102 });
4103 }
4104
4105 (
4106 median_duration(&mut samples),
4107 output_rows.expect("at least one measured execution"),
4108 executor.join_index_cache_stats(),
4109 executor.provider.host_transfer_stats(),
4110 )
4111 }
4112
4113 #[test]
4114 fn test_persistent_hash_index_reuses_across_repeated_session_evaluations() {
4115 let mut executor = match create_test_executor_with_config(
4116 RuntimeConfig::default().with_persistent_hash_indexes(Some(true)),
4117 ) {
4118 Some(e) => e,
4119 None => {
4120 eprintln!("Skipping test: no CUDA device available");
4121 return;
4122 }
4123 };
4124 seed_persistent_index_fixture(&mut executor, 2_500);
4125 let plan = persistent_index_join_plan();
4126 executor.provider.reset_host_transfer_stats();
4127
4128 warm_persistent_index(&mut executor, &plan, 5);
4129
4130 let stats = executor.join_index_cache_stats();
4131 let transfers = executor.provider.host_transfer_stats();
4132 assert_eq!(stats.builds, 1);
4133 assert!(stats.hits >= 1);
4134 assert_eq!(stats.stale_rejections, 0);
4135 assert_eq!(stats.entries, 1);
4136 assert!(stats.total_bytes > 0);
4137 assert_eq!(transfers.dtoh_calls, 0);
4138 assert_eq!(transfers.htod_calls, 0);
4139 }
4140
4141 #[test]
4142 fn test_persistent_hash_index_invalidates_on_relation_generation_change() {
4143 let mut executor = match create_test_executor_with_config(
4144 RuntimeConfig::default().with_persistent_hash_indexes(Some(true)),
4145 ) {
4146 Some(e) => e,
4147 None => {
4148 eprintln!("Skipping test: no CUDA device available");
4149 return;
4150 }
4151 };
4152 seed_persistent_index_fixture(&mut executor, 2_500);
4153 let plan = persistent_index_join_plan();
4154 warm_persistent_index(&mut executor, &plan, 5);
4155 assert_eq!(executor.join_index_cache_stats().entries, 1);
4156
4157 let changed_values: Vec<u32> = (10_000..12_500).collect();
4158 let changed_right = create_test_buffer(&executor, &changed_values, "key");
4159 executor.put_relation("right", changed_right);
4160
4161 let stats = executor.join_index_cache_stats();
4162 assert_eq!(stats.entries, 0);
4163 assert!(stats.invalidations >= 1);
4164 }
4165
4166 #[test]
4167 fn test_persistent_hash_index_background_build_records_requests() {
4168 let mut executor = match create_test_executor_with_config(
4169 RuntimeConfig::default()
4170 .with_persistent_hash_indexes(Some(true))
4171 .with_persistent_hash_index_background_build(Some(true)),
4172 ) {
4173 Some(e) => e,
4174 None => {
4175 eprintln!("Skipping test: no CUDA device available");
4176 return;
4177 }
4178 };
4179 seed_persistent_index_fixture(&mut executor, 2_500);
4180 let plan = persistent_index_join_plan();
4181
4182 warm_persistent_index(&mut executor, &plan, 5);
4183
4184 let stats = executor.join_index_cache_stats();
4185 assert_eq!(stats.background_build_requests, 1);
4186 assert_eq!(stats.background_builds_completed, 1);
4187 assert_eq!(stats.entries, 1);
4188 }
4189
4190 #[test]
4191 fn test_persistent_hash_index_background_build_defers_current_join_reuse() {
4192 let mut executor = match create_test_executor_with_config(
4193 RuntimeConfig::default()
4194 .with_persistent_hash_indexes(Some(true))
4195 .with_persistent_hash_index_background_build(Some(true)),
4196 ) {
4197 Some(e) => e,
4198 None => {
4199 eprintln!("Skipping test: no CUDA device available");
4200 return;
4201 }
4202 };
4203 seed_persistent_index_fixture(&mut executor, 2_500);
4204 let plan = persistent_index_join_plan();
4205
4206 let mut before_build = executor.join_index_cache_stats();
4207 let mut after_build = None;
4208 for _ in 0..5 {
4209 executor
4210 .execute_plan(&plan)
4211 .expect("background-build warm evaluation");
4212 let stats = executor.join_index_cache_stats();
4213 if stats.background_build_requests > before_build.background_build_requests {
4214 after_build = Some(stats);
4215 break;
4216 }
4217 before_build = stats;
4218 }
4219
4220 let after_first = after_build.expect("background build request observed");
4221 assert_eq!(after_first.background_build_requests, 1);
4222 assert_eq!(after_first.background_builds_completed, 1);
4223 assert_eq!(after_first.background_builds_deferred, 1);
4224 assert_eq!(
4225 after_first.hits, before_build.hits,
4226 "background build must not be consumed by the same evaluation that requested it"
4227 );
4228 assert_eq!(after_first.entries, 1);
4229
4230 executor
4231 .execute_plan(&plan)
4232 .expect("second evaluation reuses completed background index");
4233 let after_second = executor.join_index_cache_stats();
4234 assert_eq!(after_second.background_build_requests, 1);
4235 assert_eq!(after_second.background_builds_deferred, 1);
4236 assert!(after_second.hits >= 1);
4237 }
4238
4239 #[test]
4240 fn test_persistent_hash_index_performance_fixture_meets_speedup_target() {
4241 const LEFT_ROWS: u32 = 8;
4242 const RIGHT_ROWS: u32 = 8_000_000;
4243 const JOIN_REPETITIONS: usize = 1;
4244 const WARMUP: usize = 12;
4245 const ITERATIONS: usize = 9;
4246
4247 let mut cached = match create_test_executor_with_config(
4248 RuntimeConfig::default().with_persistent_hash_indexes(Some(true)),
4249 ) {
4250 Some(e) => e,
4251 None => {
4252 eprintln!("Skipping test: no CUDA device available");
4253 return;
4254 }
4255 };
4256 seed_persistent_index_performance_fixture(&mut cached, LEFT_ROWS, RIGHT_ROWS);
4257
4258 let mut uncached = match create_test_executor_with_config(
4259 RuntimeConfig::default().with_persistent_hash_indexes(Some(false)),
4260 ) {
4261 Some(e) => e,
4262 None => {
4263 eprintln!("Skipping test: no CUDA device available");
4264 return;
4265 }
4266 };
4267 seed_persistent_index_performance_fixture(&mut uncached, LEFT_ROWS, RIGHT_ROWS);
4268
4269 let plan = persistent_index_heavy_join_plan(JOIN_REPETITIONS);
4270 let (cached_median, cached_rows, cached_stats, cached_transfers) =
4271 measure_persistent_index_fixture(cached, &plan, WARMUP, ITERATIONS);
4272 let (uncached_median, uncached_rows, uncached_stats, uncached_transfers) =
4273 measure_persistent_index_fixture(uncached, &plan, WARMUP, ITERATIONS);
4274
4275 let speedup_ratio = uncached_median.as_secs_f64() / cached_median.as_secs_f64();
4276 eprintln!(
4277 "persistent_hash_index_perf left_rows={} right_rows={} join_repetitions={} warmup={} iterations={} \
4278 cached_median_sec={:.9} uncached_median_sec={:.9} speedup_ratio={:.3} \
4279 cached_output_rows={} uncached_output_rows={} cached_builds={} cached_hits={} \
4280 uncached_builds={} cached_dtoh_calls={} cached_htod_calls={}",
4281 LEFT_ROWS,
4282 RIGHT_ROWS,
4283 JOIN_REPETITIONS,
4284 WARMUP,
4285 ITERATIONS,
4286 cached_median.as_secs_f64(),
4287 uncached_median.as_secs_f64(),
4288 speedup_ratio,
4289 cached_rows,
4290 uncached_rows,
4291 cached_stats.builds,
4292 cached_stats.hits,
4293 uncached_stats.builds,
4294 cached_transfers.dtoh_calls,
4295 cached_transfers.htod_calls
4296 );
4297
4298 assert_eq!(cached_rows, uncached_rows);
4299 assert_eq!(cached_rows, LEFT_ROWS as u64);
4300 assert_eq!(cached_stats.builds, 1);
4301 assert!(cached_stats.hits >= ITERATIONS as u64);
4302 assert_eq!(uncached_stats.builds, 0);
4303 assert_eq!(cached_transfers.dtoh_calls, 0);
4304 assert_eq!(cached_transfers.htod_calls, 0);
4305 assert_eq!(uncached_transfers.dtoh_calls, 0);
4306 assert_eq!(uncached_transfers.htod_calls, 0);
4307 assert!(
4308 speedup_ratio >= 1.5,
4309 "persistent index speedup {:.3} below 1.5 target",
4310 speedup_ratio
4311 );
4312 }
4313
4314 #[test]
4317 fn test_reset_for_mc_relations_preserves_static_and_clears_dynamic() {
4318 let mut executor = match create_test_executor() {
4319 Some(e) => e,
4320 None => {
4321 eprintln!("Skipping: no CUDA device");
4322 return;
4323 }
4324 };
4325
4326 executor.register_relation(RelId(1), "base_rel");
4327 executor.register_relation(RelId(2), "dyn_rel");
4328
4329 let schema = Schema::new(vec![("x".to_string(), ScalarType::U32)]);
4330 let base = create_test_buffer(&executor, &[1u32], "x");
4331 let dyn_buf = create_test_buffer(&executor, &[9u32], "x");
4332 executor.put_relation("base_rel", base);
4333 executor.put_relation("dyn_rel", dyn_buf);
4334
4335 executor
4336 .reset_for_mc_relations(&["base_rel"], &[("dyn_rel", schema.clone())])
4337 .unwrap();
4338
4339 assert_eq!(
4340 buffer_row_count(&executor, executor.store().get("base_rel").unwrap()),
4341 1
4342 );
4343 assert_eq!(
4344 buffer_row_count(&executor, executor.store().get("dyn_rel").unwrap()),
4345 0
4346 );
4347 }
4348}