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