1#![forbid(unsafe_code)]
2
3use std::collections::{BTreeSet, HashMap, VecDeque};
9use std::fmt;
10use std::num::NonZeroUsize;
11use std::ops::ControlFlow;
12use std::path::Path;
13use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
14use std::sync::{Arc, Mutex, RwLock};
15
16use arrow::datatypes::SchemaRef;
17use arrow::record_batch::RecordBatch;
18use arrow::util::pretty::pretty_format_batches;
19use catalog::{InMemoryCatalog, datafusion_bridge::DataFusionCatalogBridge};
20use datafusion::dataframe::DataFrame as DataFusionDataFrame;
21use datafusion::prelude::{ParquetReadOptions, SessionContext};
22use datafusion::sql::sqlparser::{ast::visit_relations, dialect::GenericDialect, parser::Parser};
23use object_store::aws::AmazonS3Builder;
24
25use krishiv_plan::optimizer::{CostModel, Optimizer};
26use krishiv_plan::{ExecutionKind, LogicalPlan, PlanNode};
27
28pub(crate) fn build_s3_object_store(
38 bucket: &str,
39) -> object_store::Result<std::sync::Arc<dyn object_store::ObjectStore>> {
40 let mut builder = AmazonS3Builder::from_env().with_bucket_name(bucket);
41 if let Ok(endpoint) = std::env::var("AWS_ENDPOINT_URL")
42 && !endpoint.is_empty()
43 {
44 builder = builder.with_endpoint(endpoint).with_allow_http(true);
46 }
47 if let Ok(key) = std::env::var("AWS_ACCESS_KEY_ID") {
48 builder = builder.with_access_key_id(key);
49 }
50 if let Ok(secret) = std::env::var("AWS_SECRET_ACCESS_KEY") {
51 builder = builder.with_secret_access_key(secret);
52 }
53 let region = std::env::var("AWS_REGION")
54 .or_else(|_| std::env::var("AWS_DEFAULT_REGION"))
55 .unwrap_or_else(|_| "us-east-1".to_string());
56 builder = builder.with_region(region);
57 Ok(std::sync::Arc::new(builder.build()?))
58}
59
60pub mod analyze;
61pub mod catalog;
62pub mod cep_sql;
63
64pub mod connector_table;
65pub mod create_function_ddl;
66pub mod distributed_plan;
67pub mod grammar;
68pub mod incremental_view;
69pub mod introspection_sql;
70
71pub mod kafka_table;
72pub mod lakehouse;
73pub mod live_table;
74pub mod pipeline_ddl;
75pub mod pivot_sql;
76pub mod recursive_cte;
77pub mod spark_sql_ext;
79pub mod sqlstate;
80pub mod subquery;
81pub mod unnest_sql;
82
83pub mod streaming;
84pub mod streaming_table_ddl;
85pub mod streaming_tvf;
86pub mod streaming_window_plan;
87mod udf;
88mod json_functions;
89mod higher_order_functions;
90mod spark_functions;
91pub mod statement_completion;
92pub mod coverage;
93mod window_functions;
94
95pub use cep_sql::{
96 MatchRecognizeStatement, execute_streaming_match_recognize, parse_match_recognize,
97};
98pub use lakehouse::{AsOfTableRef, MergeResult, MergeTargetUnsupportedError, preprocess_as_of_sql};
99
100pub use grammar::{
101 FeatureEntry, FeatureStatus, feature_matrix, features_by_status, features_for_category,
102};
103pub use sqlstate::{SqlStateError, sqlstate_for};
104pub use streaming::{ContinuousInputError, ContinuousTableInput};
105
106pub type SqlResult<T> = Result<T, SqlError>;
108
109pub type SqlStream =
115 std::pin::Pin<Box<dyn futures::stream::Stream<Item = Result<RecordBatch, SqlError>> + Send>>;
116
117static EPHEMERAL_TABLE_COUNTER: AtomicU64 = AtomicU64::new(0);
120
121fn next_ephemeral_name(prefix: &str) -> String {
122 let id = EPHEMERAL_TABLE_COUNTER.fetch_add(1, Ordering::Relaxed);
123 format!("__{prefix}_{id}")
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131enum WindowFnRegistration {
132 Register,
134 Skip,
138}
139
140struct PlanCache {
146 map: HashMap<String, datafusion::logical_expr::LogicalPlan>,
147 order: VecDeque<String>,
148 max: usize,
149}
150
151impl PlanCache {
152 fn new(max: usize) -> Self {
153 Self {
154 map: HashMap::new(),
155 order: VecDeque::new(),
156 max,
157 }
158 }
159
160 fn get(&self, key: &str) -> Option<&datafusion::logical_expr::LogicalPlan> {
161 self.map.get(key)
162 }
163
164 fn insert(&mut self, key: String, plan: datafusion::logical_expr::LogicalPlan) {
165 if self.map.contains_key(&key) {
166 self.order.retain(|k| k != &key);
169 } else if self.map.len() >= self.max
170 && let Some(oldest) = self.order.pop_front()
171 {
172 self.map.remove(&oldest);
173 }
174 self.order.push_back(key.clone());
175 self.map.insert(key, plan);
176 }
177
178 fn clear(&mut self) {
179 self.map.clear();
180 self.order.clear();
181 }
182
183 #[cfg(test)]
184 fn is_empty(&self) -> bool {
185 self.map.is_empty()
186 }
187}
188
189#[derive(Debug, Clone, Default)]
191pub struct ParquetReaderOptions {
192 pub batch_size: Option<usize>,
194}
195
196#[derive(Debug, Clone, Default)]
198pub struct CsvReaderOptions {
199 pub delimiter: Option<char>,
201 pub has_header: Option<bool>,
203}
204
205#[derive(Debug, Clone, Default)]
207pub struct ParquetWriterOptions {
208 pub compression: Option<String>,
210 pub max_row_group_size: Option<usize>,
212}
213
214#[derive(Debug, Clone, Default)]
216pub struct CsvWriterOptions {
217 pub delimiter: Option<char>,
219 pub has_header: Option<bool>,
221}
222
223#[non_exhaustive]
225#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
226pub enum SqlError {
227 #[error("SQL query is empty")]
229 EmptyQuery,
230 #[error("table name is empty")]
232 EmptyTableName,
233 #[error("unsupported SQL feature: {feature}")]
235 Unsupported { feature: String },
236 #[error("invalid table function: {message}")]
238 InvalidTableFunction { message: String },
239 #[error("DataFusion error: {message}")]
241 DataFusion { message: String },
242 #[error(transparent)]
244 Optimizer(#[from] krishiv_plan::optimizer::OptimizerError),
245 #[error("access denied: {reason}")]
247 AccessDenied { reason: String },
248 #[error("operation {operation_id} was cancelled")]
250 OperationCancelled { operation_id: u64 },
251 #[error("query timed out after {timeout_ms} ms")]
253 Timeout { timeout_ms: u64 },
254}
255
256impl From<datafusion::error::DataFusionError> for SqlError {
257 fn from(value: datafusion::error::DataFusionError) -> Self {
258 Self::DataFusion {
259 message: value.to_string(),
260 }
261 }
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct SqlPlan {
267 query: String,
268 logical_plan: LogicalPlan,
269}
270
271impl SqlPlan {
272 pub fn query(&self) -> &str {
274 &self.query
275 }
276
277 pub fn logical_plan(&self) -> &LogicalPlan {
279 &self.logical_plan
280 }
281}
282
283const PLAN_CACHE_MAX_ENTRIES: usize = 256;
295
296fn resolve_plan_cache_max_entries() -> usize {
297 std::env::var("KRISHIV_PLAN_CACHE_MAX_ENTRIES")
298 .ok()
299 .and_then(|v| v.parse().ok())
300 .filter(|&n| n > 0)
301 .unwrap_or(PLAN_CACHE_MAX_ENTRIES)
302}
303const STREAMING_CEP_MAX_ROWS_DEFAULT: usize = 100_000;
304
305pub fn resolve_streaming_match_recognize_limit(raw: Option<&str>) -> usize {
309 raw.and_then(|s| s.parse::<usize>().ok())
310 .filter(|n| *n > 0)
311 .unwrap_or(STREAMING_CEP_MAX_ROWS_DEFAULT)
312}
313
314pub fn streaming_match_recognize_limit_from_env() -> usize {
317 resolve_streaming_match_recognize_limit(
318 std::env::var("KRISHIV_MATCH_RECOGNIZE_STREAMING_LIMIT")
319 .ok()
320 .as_deref(),
321 )
322}
323
324pub fn resolve_query_memory_limit_bytes(raw: Option<&str>) -> Option<usize> {
328 raw.and_then(|s| s.trim().parse::<usize>().ok())
329 .filter(|n| *n > 0)
330}
331
332pub fn query_memory_limit_from_env() -> Option<usize> {
343 match std::env::var("KRISHIV_QUERY_MEMORY_LIMIT_BYTES").ok() {
344 Some(raw) => resolve_query_memory_limit_bytes(Some(&raw)),
347 None => cgroup_memory_limit_bytes()
348 .map(|limit| (limit / 4) as usize)
349 .filter(|&n| n > 0),
350 }
351}
352
353pub use krishiv_common::cgroup_memory_limit_bytes;
354
355static RUNTIME_FILTERS_OVERRIDE: std::sync::atomic::AtomicU8 =
360 std::sync::atomic::AtomicU8::new(u8::MAX);
361
362#[doc(hidden)]
365pub fn set_runtime_filters_for_tests(enabled: bool) {
366 RUNTIME_FILTERS_OVERRIDE.store(u8::from(enabled), std::sync::atomic::Ordering::Relaxed);
367}
368
369pub fn runtime_filters_enabled_from_env() -> bool {
374 match RUNTIME_FILTERS_OVERRIDE.load(std::sync::atomic::Ordering::Relaxed) {
375 0 => return false,
376 1 => return true,
377 _ => {}
378 }
379 !matches!(
380 std::env::var("KRISHIV_RUNTIME_FILTERS")
381 .unwrap_or_default()
382 .trim()
383 .to_ascii_lowercase()
384 .as_str(),
385 "off" | "0" | "false" | "disabled"
386 )
387}
388
389pub fn batch_size_from_env() -> usize {
393 std::env::var("KRISHIV_BATCH_SIZE")
394 .ok()
395 .and_then(|v| v.parse::<usize>().ok())
396 .filter(|n| *n > 0)
397 .unwrap_or(8192)
398}
399
400pub fn default_parallelism_from_env() -> NonZeroUsize {
404 std::env::var("KRISHIV_TARGET_PARALLELISM")
405 .ok()
406 .and_then(|v| v.parse::<usize>().ok())
407 .and_then(NonZeroUsize::new)
408 .unwrap_or_else(|| std::thread::available_parallelism().unwrap_or(NonZeroUsize::MIN))
409}
410
411const DEFAULT_SORT_SPILL_RESERVATION_BYTES: usize = 10 * 1024 * 1024;
417
418const MIN_SORT_SPILL_RESERVATION_BYTES: usize = 64 * 1024;
421
422fn build_single_node_session_config(
436 target_partitions: NonZeroUsize,
437 memory_limit_bytes: Option<usize>,
438) -> datafusion::prelude::SessionConfig {
439 let tp = target_partitions.get();
440 let batch_size = batch_size_from_env();
441 let mut config = datafusion::prelude::SessionConfig::new()
442 .with_target_partitions(tp)
443 .with_batch_size(batch_size)
444 .with_information_schema(true)
445 .set_bool(
446 "datafusion.optimizer.enable_round_robin_repartition",
447 tp > 1,
448 )
449 .set_bool(
454 "datafusion.optimizer.enable_dynamic_filter_pushdown",
455 runtime_filters_enabled_from_env(),
456 )
457 .set_bool(
458 "datafusion.optimizer.enable_join_dynamic_filter_pushdown",
459 runtime_filters_enabled_from_env(),
460 )
461 .set_bool(
462 "datafusion.optimizer.enable_topk_dynamic_filter_pushdown",
463 runtime_filters_enabled_from_env(),
464 )
465 .set_bool(
466 "datafusion.optimizer.enable_aggregate_dynamic_filter_pushdown",
467 runtime_filters_enabled_from_env(),
468 );
469 config.options_mut().sql_parser.dialect = datafusion::common::config::Dialect::DuckDB;
477 if let Some(limit) = memory_limit_bytes {
484 let scaled = (limit / 4).clamp(
485 MIN_SORT_SPILL_RESERVATION_BYTES,
486 DEFAULT_SORT_SPILL_RESERVATION_BYTES,
487 );
488 config = config.with_sort_spill_reservation_bytes(scaled);
489 }
490 config
491}
492
493#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
497type IcebergCatalogRegistry =
498 Arc<std::sync::RwLock<Vec<(Arc<catalog::unified::KrishivCatalog>, String)>>>;
499
500#[derive(Clone)]
501pub struct SqlEngine {
502 context: SessionContext,
503 target_parallelism: NonZeroUsize,
504 krishiv_catalog: Option<Arc<RwLock<InMemoryCatalog>>>,
505 udf_registry: Option<std::sync::Arc<std::sync::RwLock<krishiv_plan::udf::UdfRegistry>>>,
506 streaming_sources: Arc<RwLock<std::collections::HashSet<String>>>,
509 streaming_registration: Arc<Mutex<()>>,
511 has_streaming_sources: Arc<AtomicBool>,
516 udf_limits: Option<krishiv_plan::udf::ResourceLimits>,
519 udf_registry_version: Arc<AtomicU64>,
523 udf_last_synced_version: Arc<AtomicU64>,
526 plan_cache: Arc<Mutex<PlanCache>>,
532 shuffle_partitions: Arc<std::sync::RwLock<Option<u32>>>,
535 table_row_counts: Arc<std::sync::RwLock<HashMap<String, u64>>>,
540 memory_limit_bytes: Option<usize>,
545 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
549 iceberg_catalogs: IcebergCatalogRegistry,
550 live_table_registry: Arc<live_table::LiveTableRegistry>,
552 incremental_view_registry: Arc<incremental_view::IncrementalViewRegistry>,
554 pipeline_registry: Arc<pipeline_ddl::PipelineRegistry>,
556 operation_registry: Arc<OperationRegistry>,
558}
559
560impl fmt::Debug for SqlEngine {
561 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562 f.debug_struct("SqlEngine")
563 .field("backend", &"datafusion")
564 .finish_non_exhaustive()
565 }
566}
567
568impl Default for SqlEngine {
569 fn default() -> Self {
570 Self::new()
571 }
572}
573
574impl SqlEngine {
575 pub fn new() -> Self {
589 Self::new_with_memory_limit(query_memory_limit_from_env())
590 }
591
592 pub fn new_with_memory_limit(memory_limit_bytes: Option<usize>) -> Self {
604 let parallelism = default_parallelism_from_env();
605 match Self::build_local(
606 None,
607 WindowFnRegistration::Register,
608 parallelism,
609 memory_limit_bytes,
610 ) {
611 Ok(engine) => engine,
612 Err(err) => {
613 tracing::warn!(
614 error = %err,
615 "SqlEngine::new: window helper UDF registration failed; \
616 window SQL functions will be unavailable, other queries are unaffected"
617 );
618 Self::build_local(
619 None,
620 WindowFnRegistration::Skip,
621 parallelism,
622 memory_limit_bytes,
623 )
624 .unwrap_or_else(|err| {
625 tracing::error!(
626 error = %err,
627 "memory-limited DataFusion runtime construction failed; \
628 falling back to an unbounded engine"
629 );
630 Self::build_local(None, WindowFnRegistration::Skip, parallelism, None)
631 .unwrap_or_else(|_| Self::build_absolute_minimal(parallelism))
632 })
633 }
634 }
635 }
636
637 pub fn try_new() -> SqlResult<Self> {
642 Self::build_local(
643 None,
644 WindowFnRegistration::Register,
645 default_parallelism_from_env(),
646 query_memory_limit_from_env(),
647 )
648 }
649
650 pub fn with_in_memory_catalog(catalog: Arc<RwLock<InMemoryCatalog>>) -> SqlResult<Self> {
652 if krishiv_common::profile_requires_fail_closed_metadata(
653 krishiv_common::resolve_durability_profile(),
654 ) {
655 return Err(SqlError::DataFusion {
656 message: String::from(
657 "InMemoryCatalog is dev-only; configure a durable REST or file-backed \
658 catalog for production deployments",
659 ),
660 });
661 }
662 Self::build_local(
663 Some(catalog),
664 WindowFnRegistration::Register,
665 default_parallelism_from_env(),
666 query_memory_limit_from_env(),
667 )
668 }
669
670 #[must_use]
681 pub fn with_target_parallelism(mut self, n: NonZeroUsize) -> Self {
682 self.target_parallelism = n;
683 self.apply_target_partitions(n);
684 self
685 }
686
687 fn apply_target_partitions(&self, n: NonZeroUsize) {
696 let state_ref = self.context.state_ref();
697 let mut state = state_ref.write();
698 let options = state.config_mut().options_mut();
699 options.execution.target_partitions = n.get();
700 options.optimizer.enable_round_robin_repartition = n.get() > 1;
701 }
702
703 pub fn target_parallelism(&self) -> NonZeroUsize {
705 self.target_parallelism
706 }
707
708 pub fn memory_limit_bytes(&self) -> Option<usize> {
710 self.memory_limit_bytes
711 }
712
713 pub fn session_context(&self) -> &SessionContext {
719 &self.context
720 }
721
722 pub fn shuffle_partitions(&self) -> Option<u32> {
724 *self
725 .shuffle_partitions
726 .read()
727 .unwrap_or_else(|e| e.into_inner())
728 }
729
730 pub fn table_row_counts(&self) -> Arc<std::sync::RwLock<HashMap<String, u64>>> {
736 Arc::clone(&self.table_row_counts)
737 }
738
739 pub fn registered_table_names(&self) -> Vec<String> {
745 let mut names = Vec::new();
746 for catalog_name in self.context.catalog_names() {
747 let Some(catalog) = self.context.catalog(&catalog_name) else {
748 continue;
749 };
750 for schema_name in catalog.schema_names() {
751 let Some(schema) = catalog.schema(&schema_name) else {
752 continue;
753 };
754 names.extend(schema.table_names());
755 }
756 }
757 names.sort();
758 names.dedup();
759 names
760 }
761
762 fn make_sql_df(&self, name: &str, dataframe: DataFusionDataFrame) -> SqlDataFrame {
765 SqlDataFrame::new(name, dataframe, self.table_row_counts())
766 .with_context(self.context.clone())
767 }
768
769 fn attach_query_metadata(&self, df: SqlDataFrame, query: &str) -> SqlDataFrame {
771 let kind = if self.is_streaming_query(query).unwrap_or(false) {
772 ExecutionKind::Streaming
773 } else {
774 ExecutionKind::Batch
775 };
776 df.with_query(query).with_execution_kind(kind)
777 }
778
779 #[must_use]
784 pub fn with_shuffle_partitions(self, n: Option<u32>) -> Self {
785 if let Ok(mut guard) = self.shuffle_partitions.write() {
786 *guard = n;
787 }
788 self
789 }
790
791 fn build_local(
801 krishiv_catalog: Option<Arc<RwLock<InMemoryCatalog>>>,
802 window_fn_registration: WindowFnRegistration,
803 target_partitions: NonZeroUsize,
804 memory_limit_bytes: Option<usize>,
805 ) -> SqlResult<Self> {
806 let streaming_sources: Arc<RwLock<std::collections::HashSet<String>>> =
810 Arc::new(RwLock::new(std::collections::HashSet::new()));
811
812 let mut state_builder = datafusion::execution::session_state::SessionStateBuilder::new()
813 .with_default_features()
814 .with_config(build_single_node_session_config(
815 target_partitions,
816 memory_limit_bytes,
817 ));
818 if let Some(limit) = memory_limit_bytes {
819 let runtime_env = datafusion::execution::runtime_env::RuntimeEnvBuilder::new()
824 .with_memory_pool(Arc::new(
825 datafusion::execution::memory_pool::FairSpillPool::new(limit),
826 ))
827 .build_arc()
828 .map_err(|e| SqlError::DataFusion {
829 message: format!(
830 "failed to build memory-limited DataFusion runtime \
831 (limit {limit} bytes): {e}"
832 ),
833 })?;
834 state_builder = state_builder.with_runtime_env(runtime_env);
835 }
836 let mut state = state_builder.build();
837 crate::connector_table::register_connector_table_factories(
841 state.table_factories_mut(),
842 streaming_sources.clone(),
843 );
844 let context = SessionContext::new_with_state(state);
845 if let Some(catalog) = &krishiv_catalog {
846 context.register_catalog(
847 "krishiv",
848 Arc::new(DataFusionCatalogBridge::new(catalog.clone())),
849 );
850 }
851 if matches!(window_fn_registration, WindowFnRegistration::Register) {
852 window_functions::register_window_functions(&context).map_err(|e| {
853 SqlError::DataFusion {
854 message: format!("failed to register window helper UDFs: {e}"),
855 }
856 })?;
857 }
858 json_functions::register_json_functions(&context).map_err(|e| SqlError::DataFusion {
861 message: format!("failed to register JSON UDFs: {e}"),
862 })?;
863 higher_order_functions::register_higher_order_spark_functions(&context).map_err(|e| {
866 SqlError::DataFusion {
867 message: format!("failed to register higher-order UDFs: {e}"),
868 }
869 })?;
870 spark_functions::register_spark_scalar_functions(&context).map_err(|e| {
872 SqlError::DataFusion {
873 message: format!("failed to register Spark scalar UDFs: {e}"),
874 }
875 })?;
876 Ok(Self {
877 context,
878 target_parallelism: target_partitions,
879 krishiv_catalog,
880 udf_registry: None,
881 streaming_sources,
882 streaming_registration: Arc::new(Mutex::new(())),
883 has_streaming_sources: Arc::new(AtomicBool::new(false)),
884 udf_limits: None,
885 udf_registry_version: Arc::new(AtomicU64::new(0)),
886 udf_last_synced_version: Arc::new(AtomicU64::new(u64::MAX)),
887 plan_cache: Arc::new(Mutex::new(PlanCache::new(resolve_plan_cache_max_entries()))),
888 shuffle_partitions: Arc::new(std::sync::RwLock::new(None)),
889 table_row_counts: Arc::new(std::sync::RwLock::new(HashMap::new())),
890 memory_limit_bytes,
891 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
892 iceberg_catalogs: Arc::new(std::sync::RwLock::new(Vec::new())),
893 live_table_registry: Arc::new(live_table::LiveTableRegistry::new()),
894 incremental_view_registry: Arc::new(incremental_view::IncrementalViewRegistry::new()),
895 pipeline_registry: Arc::new(pipeline_ddl::PipelineRegistry::new()),
896 operation_registry: Arc::new(OperationRegistry::new()),
897 })
898 }
899
900 fn build_absolute_minimal(target_partitions: NonZeroUsize) -> Self {
904 let streaming_sources: Arc<RwLock<std::collections::HashSet<String>>> =
905 Arc::new(RwLock::new(std::collections::HashSet::new()));
906 let mut state = datafusion::execution::session_state::SessionStateBuilder::new()
907 .with_default_features()
908 .with_config(build_single_node_session_config(target_partitions, None))
909 .build();
910 crate::connector_table::register_connector_table_factories(
911 state.table_factories_mut(),
912 streaming_sources.clone(),
913 );
914 let context = SessionContext::new_with_state(state);
915 Self {
916 context,
917 target_parallelism: target_partitions,
918 krishiv_catalog: None,
919 udf_registry: None,
920 streaming_sources,
921 streaming_registration: Arc::new(Mutex::new(())),
922 has_streaming_sources: Arc::new(AtomicBool::new(false)),
923 udf_limits: None,
924 udf_registry_version: Arc::new(AtomicU64::new(0)),
925 udf_last_synced_version: Arc::new(AtomicU64::new(u64::MAX)),
926 plan_cache: Arc::new(Mutex::new(PlanCache::new(resolve_plan_cache_max_entries()))),
927 shuffle_partitions: Arc::new(std::sync::RwLock::new(None)),
928 table_row_counts: Arc::new(std::sync::RwLock::new(HashMap::new())),
929 memory_limit_bytes: None,
930 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
931 iceberg_catalogs: Arc::new(std::sync::RwLock::new(Vec::new())),
932 live_table_registry: Arc::new(live_table::LiveTableRegistry::new()),
933 incremental_view_registry: Arc::new(incremental_view::IncrementalViewRegistry::new()),
934 pipeline_registry: Arc::new(pipeline_ddl::PipelineRegistry::new()),
935 operation_registry: Arc::new(OperationRegistry::new()),
936 }
937 }
938
939 pub fn register_streaming_table(
950 &self,
951 name: &str,
952 schema: arrow::datatypes::SchemaRef,
953 ) -> SqlResult<Arc<ContinuousTableInput>> {
954 let _registration = self.lock_streaming_registration()?;
955 self.validate_new_streaming_table(name, &schema)?;
956 let (table, input) = crate::streaming::create_continuous_table(schema).map_err(|e| {
957 SqlError::DataFusion {
958 message: e.to_string(),
959 }
960 })?;
961 self.register_new_streaming_provider(name, table)?;
962 self.streaming_sources
963 .write()
964 .unwrap_or_else(|e| e.into_inner())
965 .insert(name.to_string());
966 self.has_streaming_sources.store(true, Ordering::Release);
967 self.invalidate_plan_cache();
968 Ok(input)
969 }
970
971 pub fn register_streaming_table_with_capacity(
976 &self,
977 name: &str,
978 schema: arrow::datatypes::SchemaRef,
979 capacity: usize,
980 ) -> SqlResult<Arc<ContinuousTableInput>> {
981 let _registration = self.lock_streaming_registration()?;
982 self.validate_new_streaming_table(name, &schema)?;
983 let (table, input) = crate::streaming::create_continuous_table_with_capacity(
984 schema, capacity,
985 )
986 .map_err(|e| SqlError::DataFusion {
987 message: e.to_string(),
988 })?;
989 self.register_new_streaming_provider(name, table)?;
990 self.streaming_sources
991 .write()
992 .unwrap_or_else(|e| e.into_inner())
993 .insert(name.to_string());
994 self.has_streaming_sources.store(true, Ordering::Release);
995 self.invalidate_plan_cache();
996 Ok(input)
997 }
998
999 fn lock_streaming_registration(&self) -> SqlResult<std::sync::MutexGuard<'_, ()>> {
1000 self.streaming_registration
1001 .lock()
1002 .map_err(|error| SqlError::DataFusion {
1003 message: format!("streaming table registration lock poisoned: {error}"),
1004 })
1005 }
1006
1007 fn validate_new_streaming_table(
1008 &self,
1009 name: &str,
1010 schema: &arrow::datatypes::SchemaRef,
1011 ) -> SqlResult<()> {
1012 if name.trim().is_empty() {
1013 return Err(SqlError::EmptyTableName);
1014 }
1015 if schema.fields().is_empty() {
1016 return Err(SqlError::DataFusion {
1017 message: "streaming table schema must contain at least one field".into(),
1018 });
1019 }
1020 if self
1021 .context
1022 .table_exist(name)
1023 .map_err(|error| SqlError::DataFusion {
1024 message: error.to_string(),
1025 })?
1026 {
1027 return Err(SqlError::DataFusion {
1028 message: format!("table '{name}' is already registered"),
1029 });
1030 }
1031 Ok(())
1032 }
1033
1034 fn register_new_streaming_provider(
1035 &self,
1036 name: &str,
1037 table: Arc<dyn datafusion::catalog::TableProvider>,
1038 ) -> SqlResult<()> {
1039 let previous =
1040 self.context
1041 .register_table(name, table)
1042 .map_err(|error| SqlError::DataFusion {
1043 message: error.to_string(),
1044 })?;
1045 if let Some(previous) = previous {
1046 self.context
1047 .register_table(name, previous)
1048 .map_err(|error| SqlError::DataFusion {
1049 message: format!(
1050 "table '{name}' was concurrently registered and could not be restored: \
1051 {error}"
1052 ),
1053 })?;
1054 return Err(SqlError::DataFusion {
1055 message: format!("table '{name}' was concurrently registered"),
1056 });
1057 }
1058 Ok(())
1059 }
1060
1061 pub fn register_kafka_source(
1075 &self,
1076 table_name: impl AsRef<str>,
1077 schema: arrow::datatypes::SchemaRef,
1078 bootstrap_servers: impl Into<String>,
1079 topic: impl Into<String>,
1080 group_id: impl Into<String>,
1081 ) -> SqlResult<()> {
1082 let table_name = table_name.as_ref();
1083 if table_name.trim().is_empty() {
1084 return Err(SqlError::EmptyTableName);
1085 }
1086 let config = krishiv_connectors::kafka::KafkaConfig {
1087 bootstrap_servers: bootstrap_servers.into(),
1088 topic: topic.into(),
1089 group_id: group_id.into(),
1090 auto_commit_interval_ms: {
1091 let profile = krishiv_common::resolve_durability_profile();
1092 if krishiv_common::requires_manual_kafka_commit(profile) {
1093 None
1094 } else {
1095 Some(1_000)
1096 }
1097 },
1098 security_protocol: None,
1099 ssl_ca_location: None,
1100 ssl_certificate_location: None,
1101 ssl_key_location: None,
1102 ssl_key_password: None,
1103 sasl_username: None,
1104 sasl_password: None,
1105 sasl_mechanisms: None,
1106 enable_idempotence: None,
1107 transactional_id: None,
1108 };
1109 let table =
1110 crate::kafka_table::create_kafka_streaming_table(schema, config).map_err(|e| {
1111 SqlError::DataFusion {
1112 message: e.to_string(),
1113 }
1114 })?;
1115 if self
1116 .context
1117 .table_exist(table_name)
1118 .map_err(SqlError::from)?
1119 {
1120 let _ = self
1121 .context
1122 .deregister_table(table_name)
1123 .map_err(SqlError::from)?;
1124 }
1125 self.context
1126 .register_table(table_name, table)
1127 .map_err(|e| SqlError::DataFusion {
1128 message: e.to_string(),
1129 })?;
1130 self.streaming_sources
1131 .write()
1132 .unwrap_or_else(|e| e.into_inner())
1133 .insert(table_name.to_string());
1134 self.has_streaming_sources.store(true, Ordering::Release);
1135 self.invalidate_plan_cache();
1136 Ok(())
1137 }
1138
1139 pub async fn sql_to_kafka(
1149 &self,
1150 sql: impl AsRef<str>,
1151 bootstrap_servers: impl Into<String>,
1152 topic: impl Into<String>,
1153 ) -> SqlResult<u64> {
1154 use futures::StreamExt;
1155 use krishiv_connectors::Sink as _;
1156 use krishiv_connectors::kafka::{KafkaConfig, KafkaSink};
1157
1158 let config = KafkaConfig {
1159 bootstrap_servers: bootstrap_servers.into(),
1160 topic: topic.into(),
1161 group_id: "krishiv-sql-writer".into(),
1162 auto_commit_interval_ms: None,
1163 security_protocol: None,
1164 ssl_ca_location: None,
1165 ssl_certificate_location: None,
1166 ssl_key_location: None,
1167 ssl_key_password: None,
1168 sasl_username: None,
1169 sasl_password: None,
1170 sasl_mechanisms: None,
1171 enable_idempotence: None,
1172 transactional_id: None,
1173 };
1174 let mut sink = KafkaSink::new(config).map_err(|e| SqlError::DataFusion {
1175 message: e.to_string(),
1176 })?;
1177
1178 let df = self.sql(sql.as_ref()).await?;
1179 let mut stream = df.execute_stream().await?;
1180 let mut total_rows = 0u64;
1181
1182 while let Some(result) = stream.next().await {
1183 let batch = result.map_err(|e| SqlError::DataFusion {
1184 message: e.to_string(),
1185 })?;
1186 if batch.num_rows() > 0 {
1187 total_rows += batch.num_rows() as u64;
1188 sink.write_batch(batch)
1189 .await
1190 .map_err(|e| SqlError::DataFusion {
1191 message: e.to_string(),
1192 })?;
1193 }
1194 }
1195 sink.flush().await.map_err(|e| SqlError::DataFusion {
1196 message: e.to_string(),
1197 })?;
1198 Ok(total_rows)
1199 }
1200
1201 pub fn with_udf_limits(mut self, limits: krishiv_plan::udf::ResourceLimits) -> Self {
1205 self.udf_limits = Some(limits);
1206 self
1207 }
1208
1209 pub fn is_streaming_source(&self, table_name: &str) -> bool {
1211 self.streaming_sources
1212 .read()
1213 .unwrap_or_else(|e| e.into_inner())
1214 .contains(table_name)
1215 }
1216
1217 pub fn register_streaming_source_name(&self, table_name: impl Into<String>) -> SqlResult<()> {
1226 let name: String = table_name.into();
1227 if name.trim().is_empty() {
1228 return Err(SqlError::EmptyTableName);
1229 }
1230 self.streaming_sources
1231 .write()
1232 .unwrap_or_else(|e| e.into_inner())
1233 .insert(name);
1234 self.has_streaming_sources.store(true, Ordering::Release);
1235 self.invalidate_plan_cache();
1236 Ok(())
1237 }
1238
1239 pub fn deregister_streaming_source(&self, name: &str) -> SqlResult<()> {
1245 if name.trim().is_empty() {
1246 return Err(SqlError::EmptyTableName);
1247 }
1248 let _ = self
1250 .context
1251 .deregister_table(name)
1252 .map_err(SqlError::from)?;
1253 {
1254 let mut sources = self
1255 .streaming_sources
1256 .write()
1257 .unwrap_or_else(|e| e.into_inner());
1258 sources.remove(name);
1259 if sources.is_empty() {
1260 self.has_streaming_sources.store(false, Ordering::Release);
1261 }
1262 self.invalidate_plan_cache();
1266 }
1267 Ok(())
1268 }
1269
1270 pub fn live_table_registry(&self) -> &Arc<live_table::LiveTableRegistry> {
1272 &self.live_table_registry
1273 }
1274
1275 pub fn incremental_view_registry(&self) -> &Arc<incremental_view::IncrementalViewRegistry> {
1277 &self.incremental_view_registry
1278 }
1279
1280 pub fn pipeline_registry(&self) -> &Arc<pipeline_ddl::PipelineRegistry> {
1282 &self.pipeline_registry
1283 }
1284
1285 pub fn operation_registry(&self) -> &Arc<OperationRegistry> {
1287 &self.operation_registry
1288 }
1289
1290 pub fn deregister_table(&self, name: &str) -> SqlResult<()> {
1294 if name.trim().is_empty() {
1295 return Err(SqlError::EmptyTableName);
1296 }
1297 let _ = self
1298 .context
1299 .deregister_table(name)
1300 .map_err(SqlError::from)?;
1301 self.invalidate_plan_cache();
1302 Ok(())
1303 }
1304
1305 pub fn register_table_udf_fn(
1329 &self,
1330 name: impl Into<String>,
1331 schema: arrow::datatypes::Schema,
1332 f: impl Fn(
1333 &[krishiv_plan::udf::ScalarValue],
1334 ) -> Result<arrow::record_batch::RecordBatch, krishiv_plan::udf::UdfError>
1335 + Send
1336 + Sync
1337 + 'static,
1338 ) -> SqlResult<()> {
1339 let udf =
1340 create_function_ddl::ClosureTableUdf::try_new(name, schema, std::sync::Arc::new(f))
1341 .map_err(|error| SqlError::InvalidTableFunction {
1342 message: error.to_string(),
1343 })?;
1344 if let Some(registry) = &self.udf_registry {
1345 let mut guard = registry.write().map_err(|e| SqlError::DataFusion {
1346 message: e.to_string(),
1347 })?;
1348 guard.register_table(std::sync::Arc::new(udf.clone()));
1349 }
1350 udf::register_single_table_udf(&self.context, std::sync::Arc::new(udf))
1351 .map_err(SqlError::from)
1352 }
1353
1354 pub fn is_streaming_query(&self, sql: &str) -> SqlResult<bool> {
1356 if !self.has_streaming_sources.load(Ordering::Acquire) {
1359 return Ok(false);
1360 }
1361 let sources = self
1362 .streaming_sources
1363 .read()
1364 .unwrap_or_else(|e| e.into_inner());
1365 if sources.is_empty() {
1366 return Ok(false);
1367 }
1368 let dialect = GenericDialect {};
1369 let statements = Parser::parse_sql(&dialect, sql).map_err(|e| SqlError::DataFusion {
1370 message: e.to_string(),
1371 })?;
1372 for stmt in &statements {
1373 let mut is_streaming = false;
1374 let _ = visit_relations(stmt, |relation| {
1375 let full = relation.to_string();
1378 let table_name = full.split('.').next_back().unwrap_or(&full);
1379 if sources.contains(table_name) {
1380 is_streaming = true;
1381 return ControlFlow::Break(());
1382 }
1383 ControlFlow::Continue(())
1384 });
1385 if is_streaming {
1386 return Ok(true);
1387 }
1388 }
1389 Ok(false)
1390 }
1391
1392 pub fn krishiv_catalog(&self) -> Option<&Arc<RwLock<InMemoryCatalog>>> {
1394 self.krishiv_catalog.as_ref()
1395 }
1396
1397 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
1406 #[must_use]
1407 pub fn with_iceberg_catalog(
1408 self,
1409 catalog: std::sync::Arc<catalog::unified::KrishivCatalog>,
1410 catalog_name: impl Into<String>,
1411 ) -> Self {
1412 self.register_iceberg_catalog(catalog, catalog_name);
1413 self
1414 }
1415
1416 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
1423 pub fn register_iceberg_catalog(
1424 &self,
1425 catalog: std::sync::Arc<catalog::unified::KrishivCatalog>,
1426 catalog_name: impl Into<String>,
1427 ) {
1428 let catalog_name = catalog_name.into();
1429 let bridge = catalog::iceberg_catalog_bridge::IcebergCatalogBridge::new(
1430 Arc::clone(&catalog),
1431 catalog_name.clone(),
1432 );
1433 self.context
1434 .register_catalog(catalog_name.clone(), Arc::new(bridge));
1435 self.iceberg_catalogs
1436 .write()
1437 .unwrap_or_else(|e| e.into_inner())
1438 .push((catalog, catalog_name));
1439 self.invalidate_plan_cache();
1440 }
1441
1442 pub async fn register_iceberg_rest_catalog_from_env(&self) -> Result<bool, String> {
1454 #[cfg(feature = "rest-catalog")]
1455 {
1456 let uri = match std::env::var("KRISHIV_ICEBERG_REST_URI") {
1457 Ok(uri) => uri,
1458 Err(_) => return Ok(false),
1459 };
1460 let warehouse = std::env::var("KRISHIV_ICEBERG_REST_WAREHOUSE").unwrap_or_default();
1461 let token = std::env::var("KRISHIV_ICEBERG_REST_TOKEN").ok();
1462 let name =
1467 std::env::var("KRISHIV_ICEBERG_REST_NAME").unwrap_or_else(|_| String::from("main"));
1468 self.register_s3_object_store_for_warehouse(&warehouse)?;
1474 let catalog = std::sync::Arc::new(
1475 catalog::unified::KrishivCatalog::rest(&uri, &warehouse, token.as_deref())
1476 .await
1477 .map_err(|e| format!("iceberg REST catalog at {uri}: {e}"))?,
1478 );
1479 self.register_iceberg_catalog(std::sync::Arc::clone(&catalog), &name);
1480 if name != "krishiv" {
1486 self.register_iceberg_catalog(catalog, "krishiv");
1487 }
1488 Ok(true)
1489 }
1490 #[cfg(not(feature = "rest-catalog"))]
1491 {
1492 Ok(false)
1493 }
1494 }
1495
1496 #[must_use]
1498 pub fn with_udf_registry(
1499 mut self,
1500 registry: std::sync::Arc<std::sync::RwLock<krishiv_plan::udf::UdfRegistry>>,
1501 ) -> Self {
1502 self.udf_registry = Some(registry);
1503 self.bump_udf_version();
1505 self
1506 }
1507
1508 pub(crate) fn bump_udf_version(&self) {
1511 self.udf_registry_version.fetch_add(1, Ordering::Release);
1512 }
1513
1514 fn invalidate_plan_cache(&self) {
1519 match self.plan_cache.lock() {
1520 Ok(mut cache) => cache.clear(),
1521 Err(poisoned) => poisoned.into_inner().clear(),
1522 }
1523 }
1524
1525 pub fn clear_plan_cache(&self) {
1528 self.invalidate_plan_cache();
1529 }
1530
1531 pub async fn sync_scalar_udfs(&self) -> SqlResult<()> {
1534 let Some(registry) = &self.udf_registry else {
1535 return Ok(());
1536 };
1537 let guard = registry.read().map_err(|e| SqlError::DataFusion {
1538 message: e.to_string(),
1539 })?;
1540 let limits = self.udf_limits.clone().unwrap_or_default();
1541 udf::sync_scalar_udfs_with_limits(&self.context, &guard, limits).map_err(|e| {
1542 SqlError::DataFusion {
1543 message: e.to_string(),
1544 }
1545 })
1546 }
1547
1548 pub async fn sync_scalar_udfs_with_limits(
1553 &self,
1554 limits: krishiv_plan::udf::ResourceLimits,
1555 ) -> SqlResult<()> {
1556 self.sync_scalar_udfs_with_limits_for_profile(
1557 limits,
1558 krishiv_common::resolve_durability_profile(),
1559 )
1560 .await
1561 }
1562
1563 pub async fn sync_scalar_udfs_with_limits_for_profile(
1565 &self,
1566 limits: krishiv_plan::udf::ResourceLimits,
1567 profile: krishiv_common::DurabilityProfile,
1568 ) -> SqlResult<()> {
1569 self.sync_scalar_udfs_with_limits_for_policy(
1570 limits,
1571 krishiv_common::NativeScalarUdfPolicy::resolve(profile),
1572 )
1573 .await
1574 }
1575
1576 pub async fn sync_scalar_udfs_with_limits_for_policy(
1578 &self,
1579 limits: krishiv_plan::udf::ResourceLimits,
1580 policy: krishiv_common::NativeScalarUdfPolicy,
1581 ) -> SqlResult<()> {
1582 let Some(registry) = &self.udf_registry else {
1583 return Ok(());
1584 };
1585 let guard = registry.read().map_err(|e| SqlError::DataFusion {
1586 message: e.to_string(),
1587 })?;
1588 udf::sync_scalar_udfs_with_limits_for_policy(&self.context, &guard, limits, policy).map_err(
1589 |e| SqlError::DataFusion {
1590 message: e.to_string(),
1591 },
1592 )
1593 }
1594
1595 pub async fn sync_aggregate_udfs(&self) -> SqlResult<()> {
1597 let Some(registry) = &self.udf_registry else {
1598 return Ok(());
1599 };
1600 let guard = registry.read().map_err(|e| SqlError::DataFusion {
1601 message: e.to_string(),
1602 })?;
1603 udf::sync_aggregate_udfs(&self.context, &guard).map_err(|e| SqlError::DataFusion {
1604 message: e.to_string(),
1605 })
1606 }
1607
1608 pub async fn sync_table_udfs(&self) -> SqlResult<()> {
1610 let Some(registry) = &self.udf_registry else {
1611 return Ok(());
1612 };
1613 let guard = registry.read().map_err(|e| SqlError::DataFusion {
1614 message: e.to_string(),
1615 })?;
1616 udf::sync_table_udfs(&self.context, &guard).map_err(|e| SqlError::DataFusion {
1617 message: e.to_string(),
1618 })
1619 }
1620
1621 pub async fn sync_all_udfs(&self) -> SqlResult<()> {
1623 self.sync_scalar_udfs().await?;
1624 self.sync_aggregate_udfs().await?;
1625 self.sync_table_udfs().await?;
1626 Ok(())
1627 }
1628
1629 pub(crate) fn register_s3_object_store_for_warehouse(&self, path: &str) -> Result<(), String> {
1636 if !(path.starts_with("s3://") || path.starts_with("s3a://")) {
1637 return Ok(());
1638 }
1639 let url = url::Url::parse(path).map_err(|e| format!("invalid s3 url {path}: {e}"))?;
1640 let bucket = url.host_str().unwrap_or_default();
1641 let store_url = url::Url::parse(&format!("s3://{bucket}"))
1643 .map_err(|e| format!("invalid s3 bucket url: {e}"))?;
1644 let store = build_s3_object_store(bucket).map_err(|e| format!("s3 store init: {e}"))?;
1645 self.context.register_object_store(&store_url, store);
1646 Ok(())
1647 }
1648
1649 pub async fn register_parquet(
1651 &self,
1652 table_name: impl AsRef<str>,
1653 path: impl AsRef<Path>,
1654 ) -> SqlResult<()> {
1655 let table_name = table_name.as_ref();
1656 if table_name.trim().is_empty() {
1657 return Err(SqlError::EmptyTableName);
1658 }
1659
1660 let path = path.as_ref().to_string_lossy().into_owned();
1661
1662 self.register_s3_object_store_for_warehouse(&path)
1665 .map_err(|message| SqlError::DataFusion { message })?;
1666
1667 if self
1668 .context
1669 .table_exist(table_name)
1670 .map_err(SqlError::from)?
1671 {
1672 let _ = self
1673 .context
1674 .deregister_table(table_name)
1675 .map_err(SqlError::from)?;
1676 }
1677 self.context
1678 .register_parquet(table_name, path, ParquetReadOptions::default())
1679 .await?;
1680 if let Ok(provider) = self.context.table_provider(table_name).await
1682 && let Some(stats) = provider.statistics()
1683 && let Some(n) = stats.num_rows.get_value()
1684 && let Ok(mut counts) = self.table_row_counts.write()
1685 {
1686 counts.insert(table_name.to_string(), *n as u64);
1687 }
1688 self.invalidate_plan_cache();
1689 Ok(())
1690 }
1691
1692 pub async fn read_parquet(&self, path: impl AsRef<Path>) -> SqlResult<SqlDataFrame> {
1694 let path = path.as_ref().to_string_lossy().into_owned();
1695 let dataframe = self
1696 .context
1697 .read_parquet(path, ParquetReadOptions::default())
1698 .await?;
1699 Ok(self.make_sql_df("parquet-read", dataframe))
1700 }
1701
1702 pub async fn register_record_batches(
1708 &self,
1709 table_name: impl AsRef<str>,
1710 batches: Vec<RecordBatch>,
1711 ) -> SqlResult<()> {
1712 use std::sync::Arc;
1713 let table_name = table_name.as_ref();
1714 if table_name.trim().is_empty() {
1715 return Err(SqlError::EmptyTableName);
1716 }
1717 if batches.is_empty() {
1718 return Ok(());
1719 }
1720 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
1721 let schema = batches
1722 .first()
1723 .ok_or_else(|| SqlError::DataFusion {
1724 message: "empty batch list".into(),
1725 })?
1726 .schema();
1727 let mem_table =
1728 datafusion::datasource::MemTable::try_new(schema, vec![batches]).map_err(|e| {
1729 SqlError::DataFusion {
1730 message: e.to_string(),
1731 }
1732 })?;
1733 if self
1734 .context
1735 .table_exist(table_name)
1736 .map_err(SqlError::from)?
1737 {
1738 let _ = self
1739 .context
1740 .deregister_table(table_name)
1741 .map_err(SqlError::from)?;
1742 }
1743 self.context
1744 .register_table(table_name, Arc::new(mem_table))
1745 .map_err(|e| SqlError::DataFusion {
1746 message: e.to_string(),
1747 })?;
1748 if total_rows > 0
1749 && let Ok(mut counts) = self.table_row_counts.write()
1750 {
1751 counts.insert(table_name.to_string(), total_rows as u64);
1752 }
1753 self.invalidate_plan_cache();
1754 Ok(())
1755 }
1756
1757 pub async fn read_parquet_with_options(
1759 &self,
1760 path: impl AsRef<Path>,
1761 opts: &ParquetReaderOptions,
1762 ) -> SqlResult<SqlDataFrame> {
1763 let path = path.as_ref().to_string_lossy().into_owned();
1764 let mut options = datafusion::prelude::ParquetReadOptions::default();
1765 if opts.batch_size.is_some() {
1766 options = options.parquet_pruning(true);
1767 }
1768 let dataframe = self.context.read_parquet(path, options).await?;
1774 Ok(self.make_sql_df("parquet-read", dataframe))
1775 }
1776
1777 pub async fn read_csv(&self, path: impl AsRef<Path>) -> SqlResult<SqlDataFrame> {
1779 self.read_csv_with_options(path, &CsvReaderOptions::default())
1780 .await
1781 }
1782
1783 pub async fn read_csv_with_options(
1785 &self,
1786 path: impl AsRef<Path>,
1787 opts: &CsvReaderOptions,
1788 ) -> SqlResult<SqlDataFrame> {
1789 let path = path.as_ref().to_string_lossy().into_owned();
1790 let mut options = datafusion::prelude::CsvReadOptions::new();
1791 if let Some(delim) = opts.delimiter {
1792 options = options.delimiter(delim as u8);
1793 }
1794 if let Some(has_header) = opts.has_header {
1795 options = options.has_header(has_header);
1796 }
1797 let dataframe = self.context.read_csv(path, options).await?;
1798 Ok(self.make_sql_df("csv-read", dataframe))
1799 }
1800
1801 pub async fn read_json(&self, path: impl AsRef<Path>) -> SqlResult<SqlDataFrame> {
1803 let path = path.as_ref().to_string_lossy().into_owned();
1804 let dataframe = self
1805 .context
1806 .read_json(path, datafusion::prelude::JsonReadOptions::default())
1807 .await?;
1808 Ok(self.make_sql_df("json-read", dataframe))
1809 }
1810
1811 pub async fn read_delta(
1813 &self,
1814 path: impl AsRef<str>,
1815 version: Option<i64>,
1816 ) -> SqlResult<SqlDataFrame> {
1817 let path = path.as_ref();
1818 let base = path.replace(['/', '.', '-'], "_");
1819 let table = match version {
1820 Some(v) => format!("delta_{base}_v{v}"),
1821 None => format!("delta_{base}"),
1822 };
1823 lakehouse::register_delta_uri(&self.context, &table, path, version).await?;
1824 self.sql(format!("SELECT * FROM {table}")).await
1825 }
1826
1827 pub async fn read_hudi(
1829 &self,
1830 path: impl AsRef<str>,
1831 query_type: krishiv_connectors::lakehouse::HudiQueryType,
1832 begin_instant: Option<&str>,
1833 ) -> SqlResult<SqlDataFrame> {
1834 let path = path.as_ref();
1835 let table = format!("hudi_{}", path.replace(['/', '.', '-'], "_"));
1836 lakehouse::register_hudi_uri(&self.context, &table, path, query_type, begin_instant)
1837 .await?;
1838 self.sql(format!("SELECT * FROM {table}")).await
1839 }
1840
1841 pub async fn sql(&self, query: impl AsRef<str>) -> SqlResult<SqlDataFrame> {
1843 let query = query.as_ref();
1844 if query.trim().is_empty() {
1845 return Err(SqlError::EmptyQuery);
1846 }
1847
1848 {
1852 let current = self.udf_registry_version.load(Ordering::Acquire);
1853 let last = self.udf_last_synced_version.load(Ordering::Relaxed);
1854 if current != last {
1855 self.sync_all_udfs().await?;
1856 self.udf_last_synced_version
1857 .store(current, Ordering::Release);
1858 }
1859 }
1860
1861 if let Some(stmt) = introspection_sql::parse_introspection_statement(query)? {
1863 return match stmt {
1864 introspection_sql::IntrospectionStatement::Describe { table } => {
1865 let batch = introspection_sql::describe_table(&self.context, &table).await?;
1866 let describe_table_name = next_ephemeral_name("describe_result");
1867 lakehouse::register_scan_batches(
1868 &self.context,
1869 &describe_table_name,
1870 vec![batch],
1871 )
1872 .await?;
1873 let dataframe = self
1874 .context
1875 .sql(&format!("SELECT * FROM {describe_table_name}"))
1876 .await?;
1877 Ok(self.attach_query_metadata(self.make_sql_df("describe", dataframe), query))
1878 }
1879 introspection_sql::IntrospectionStatement::Explain { mode, query: inner } => {
1880 let text = introspection_sql::explain_query(&inner, mode)?;
1881 let batch = introspection_sql::explain_result_batch(&text)?;
1882 let explain_table = next_ephemeral_name("explain_result");
1883 lakehouse::register_scan_batches(&self.context, &explain_table, vec![batch])
1884 .await?;
1885 let dataframe = self
1886 .context
1887 .sql(&format!("SELECT * FROM {explain_table}"))
1888 .await?;
1889 Ok(self.attach_query_metadata(self.make_sql_df("explain", dataframe), query))
1890 }
1891 };
1892 }
1893
1894 if live_table::execute_live_table_ddl(&self.live_table_registry, query)?.is_some() {
1896 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
1897 return Ok(self.attach_query_metadata(self.make_sql_df("live-table-ddl", empty), query));
1898 }
1899
1900 match incremental_view::execute_incremental_view_ddl(
1902 &self.incremental_view_registry,
1903 query,
1904 )? {
1905 Some(incremental_view::IncrementalViewResult::Refresh(_name)) => {
1906 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
1909 return Ok(self.attach_query_metadata(
1910 self.make_sql_df("incremental-view-refresh", empty),
1911 query,
1912 ));
1913 }
1914 Some(_) => {
1915 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
1916 return Ok(self.attach_query_metadata(
1917 self.make_sql_df("incremental-view-ddl", empty),
1918 query,
1919 ));
1920 }
1921 None => {}
1922 }
1923
1924 if let Some(ddl) = streaming_table_ddl::parse_create_streaming_table(query) {
1932 let _plan = streaming_window_plan::compile_streaming_window_sql(&ddl.query)?;
1933 return Err(SqlError::Unsupported {
1934 feature: format!(
1935 "CREATE STREAMING TABLE '{}' compiled to a continuous plan, but this session \
1936 has no streaming coordinator to run it; submit it via the continuous-stream \
1937 registration API or a cluster-attached session",
1938 ddl.name
1939 ),
1940 });
1941 }
1942
1943 if pipeline_ddl::execute_pipeline_ddl(&self.pipeline_registry, query)?.is_some() {
1947 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
1948 return Ok(self.attach_query_metadata(self.make_sql_df("pipeline-ddl", empty), query));
1949 }
1950
1951 let trimmed = query.trim();
1954 if trimmed
1955 .to_ascii_uppercase()
1956 .starts_with("SET SHUFFLE.PARTITIONS")
1957 {
1958 let value = trimmed.split('=').nth(1).map(|s| s.trim()).unwrap_or("");
1959 match value.parse::<u32>() {
1960 Ok(n) if n > 0 => {
1961 {
1962 let mut guard =
1963 self.shuffle_partitions
1964 .write()
1965 .map_err(|e| SqlError::DataFusion {
1966 message: e.to_string(),
1967 })?;
1968 *guard = Some(n);
1969 }
1970 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
1971 return Ok(self.make_sql_df("set-shuffle-partitions", empty));
1972 }
1973 Ok(_) => {
1974 {
1975 let mut guard =
1976 self.shuffle_partitions
1977 .write()
1978 .map_err(|e| SqlError::DataFusion {
1979 message: e.to_string(),
1980 })?;
1981 *guard = None;
1982 }
1983 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
1984 return Ok(self.make_sql_df("set-shuffle-partitions", empty));
1985 }
1986 Err(_) => {
1987 return Err(SqlError::DataFusion {
1988 message: format!(
1989 "invalid shuffle.partitions value '{value}'; expected a positive integer"
1990 ),
1991 });
1992 }
1993 }
1994 }
1995
1996 if let Some(result) = statement_completion::apply_use(&self.context, query) {
2000 result.map_err(|message| SqlError::DataFusion { message })?;
2001 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2002 return Ok(self.attach_query_metadata(self.make_sql_df("use", empty), query));
2003 }
2004 if let Some(rewrite) = statement_completion::rewrite_show_databases(query) {
2005 let dataframe = self.context.sql(&rewrite).await?;
2006 return Ok(
2007 self.attach_query_metadata(self.make_sql_df("show-databases", dataframe), query)
2008 );
2009 }
2010
2011 if create_function_ddl::is_create_function_returns_table(query) {
2016 let ddl = create_function_ddl::parse_create_function(query)
2017 .map_err(|message| SqlError::InvalidTableFunction { message })?;
2018 if ddl.language.as_deref() != Some("sql") {
2019 return Err(SqlError::Unsupported {
2020 feature: format!(
2021 "CREATE FUNCTION '{}' uses language {:?}; only LANGUAGE SQL AS '...' \
2022 table functions are executable",
2023 ddl.function_name, ddl.language
2024 ),
2025 });
2026 }
2027 let body = ddl
2028 .body
2029 .as_deref()
2030 .filter(|body| !body.trim().is_empty())
2031 .ok_or_else(|| SqlError::InvalidTableFunction {
2032 message: format!(
2033 "SQL table function '{}' requires a non-empty AS body",
2034 ddl.function_name
2035 ),
2036 })?;
2037 let fields: Vec<_> = ddl
2038 .return_columns
2039 .iter()
2040 .map(|column| {
2041 arrow::datatypes::Field::new(&column.name, column.data_type.clone(), true)
2042 })
2043 .collect();
2044 let schema = arrow::datatypes::Schema::new(fields);
2045 let udf: std::sync::Arc<dyn krishiv_plan::udf::TableUdf> = std::sync::Arc::new(
2046 create_function_ddl::SqlBodyTableUdf::try_new(
2047 &ddl.function_name,
2048 schema,
2049 body,
2050 ddl.arguments.len(),
2051 std::sync::Arc::new(self.context.clone()),
2052 )
2053 .map_err(|error| SqlError::InvalidTableFunction {
2054 message: error.to_string(),
2055 })?,
2056 );
2057 if let Some(registry) = &self.udf_registry {
2058 let mut guard = registry.write().map_err(|e| SqlError::DataFusion {
2059 message: e.to_string(),
2060 })?;
2061 guard.register_table(std::sync::Arc::clone(&udf));
2062 }
2063 udf::register_single_table_udf(&self.context, std::sync::Arc::clone(&udf))
2064 .map_err(SqlError::from)?;
2065 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2066 return Ok(
2067 self.attach_query_metadata(self.make_sql_df("create-function", empty), query)
2068 );
2069 }
2070
2071 if query
2072 .trim_start()
2073 .to_ascii_uppercase()
2074 .starts_with("MERGE INTO")
2075 {
2076 let batches = lakehouse::execute_merge_sql(&self.context, query).await?;
2077 let merge_table = next_ephemeral_name("merge_result");
2078 lakehouse::register_scan_batches(&self.context, &merge_table, batches).await?;
2079 let dataframe = self
2080 .context
2081 .sql(&format!("SELECT * FROM {merge_table}"))
2082 .await?;
2083 return Ok(self.attach_query_metadata(self.make_sql_df("merge", dataframe), query));
2084 }
2085
2086 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2095 if trimmed.to_ascii_uppercase().starts_with("CREATE ")
2096 && let Some(parsed_ctas) = parse_ctas(trimmed)
2097 {
2098 let resolved = self.resolve_iceberg_table(&parsed_ctas.table_ref);
2099 if resolved.is_none() && !parsed_ctas.partition_by.is_empty() {
2102 return Err(SqlError::DataFusion {
2103 message: format!(
2104 "PARTITIONED BY requires an Iceberg catalog table; `{}` does not \
2105 resolve to a registered Iceberg catalog",
2106 parsed_ctas.table_ref
2107 ),
2108 });
2109 }
2110 if let Some((iceberg_catalog, table_ident)) = resolved {
2111 return self
2112 .execute_iceberg_ctas(iceberg_catalog, table_ident, parsed_ctas, query)
2113 .await;
2114 }
2115 }
2116
2117 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2120 if trimmed.to_ascii_uppercase().starts_with("CALL SYSTEM.") {
2121 let result = self.dispatch_call_system(trimmed).await?;
2122 let call_table = next_ephemeral_name("call_result");
2123 lakehouse::register_scan_batches(&self.context, &call_table, vec![result]).await?;
2124 let dataframe = self
2125 .context
2126 .sql(&format!("SELECT * FROM {call_table}"))
2127 .await?;
2128 return Ok(self.attach_query_metadata(self.make_sql_df("call", dataframe), query));
2129 }
2130
2131 if trimmed
2137 .get(..14)
2138 .is_some_and(|p| p.eq_ignore_ascii_case("ANALYZE TABLE "))
2139 {
2140 let result = self.dispatch_analyze_table(trimmed).await?;
2141 let res_table = next_ephemeral_name("analyze_result");
2142 lakehouse::register_scan_batches(&self.context, &res_table, vec![result]).await?;
2143 let dataframe = self
2144 .context
2145 .sql(&format!("SELECT * FROM {res_table}"))
2146 .await?;
2147 return Ok(self.attach_query_metadata(self.make_sql_df("analyze", dataframe), query));
2148 }
2149
2150 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2154 if trimmed.to_ascii_uppercase().starts_with("DELETE FROM ")
2155 && let Some((table_ref, predicate)) = parse_dml_delete(trimmed)
2156 && let Some((iceberg_catalog, table_ident)) = self.resolve_iceberg_table(&table_ref)
2157 {
2158 use arrow::array::{ArrayRef, Int64Array};
2159 use arrow::datatypes::{DataType, Field, Schema};
2160 let (deleted, _) = krishiv_connectors::lakehouse::dml::iceberg_delete_where(
2161 iceberg_catalog,
2162 &table_ident,
2163 &predicate,
2164 &self.context,
2165 )
2166 .await
2167 .map_err(|e| SqlError::DataFusion {
2168 message: e.to_string(),
2169 })?;
2170 self.adjust_table_row_count_stat(&table_ref, -(deleted as i64));
2172 let schema = Arc::new(Schema::new(vec![Field::new(
2173 "deleted_rows",
2174 DataType::Int64,
2175 false,
2176 )]));
2177 let array: ArrayRef = Arc::new(Int64Array::from(vec![deleted as i64]));
2178 let batch =
2179 RecordBatch::try_new(schema, vec![array]).map_err(|e| SqlError::DataFusion {
2180 message: e.to_string(),
2181 })?;
2182 let res_table = next_ephemeral_name("delete_result");
2183 lakehouse::register_scan_batches(&self.context, &res_table, vec![batch]).await?;
2184 let dataframe = self
2185 .context
2186 .sql(&format!("SELECT * FROM {res_table}"))
2187 .await?;
2188 return Ok(self.attach_query_metadata(self.make_sql_df("delete", dataframe), query));
2189 }
2190
2191 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2193 if trimmed.to_ascii_uppercase().starts_with("UPDATE ")
2194 && let Some(parsed) = parse_dml_update(trimmed)
2195 && let Some((iceberg_catalog, table_ident)) =
2196 self.resolve_iceberg_table(&parsed.table_ref)
2197 {
2198 use arrow::array::{ArrayRef, Int64Array};
2199 use arrow::datatypes::{DataType, Field, Schema};
2200 let borrowed: Vec<(&str, &str)> = parsed
2201 .assignments
2202 .iter()
2203 .map(|(c, e)| (c.as_str(), e.as_str()))
2204 .collect();
2205 let pred = parsed.predicate.as_deref();
2206 let (updated, _) = krishiv_connectors::lakehouse::dml::iceberg_update_where(
2207 iceberg_catalog,
2208 &table_ident,
2209 &borrowed,
2210 pred,
2211 &self.context,
2212 )
2213 .await
2214 .map_err(|e| SqlError::DataFusion {
2215 message: e.to_string(),
2216 })?;
2217 let schema = Arc::new(Schema::new(vec![Field::new(
2218 "updated_rows",
2219 DataType::Int64,
2220 false,
2221 )]));
2222 let array: ArrayRef = Arc::new(Int64Array::from(vec![updated as i64]));
2223 let batch =
2224 RecordBatch::try_new(schema, vec![array]).map_err(|e| SqlError::DataFusion {
2225 message: e.to_string(),
2226 })?;
2227 let res_table = next_ephemeral_name("update_result");
2228 lakehouse::register_scan_batches(&self.context, &res_table, vec![batch]).await?;
2229 let dataframe = self
2230 .context
2231 .sql(&format!("SELECT * FROM {res_table}"))
2232 .await?;
2233 return Ok(self.attach_query_metadata(self.make_sql_df("update", dataframe), query));
2234 }
2235
2236 if query.to_ascii_uppercase().contains(" MATCH_RECOGNIZE ")
2240 && let Some(stmt) = cep_sql::parse_match_recognize(query)?
2241 {
2242 let is_streaming = self.is_streaming_source(&stmt.source_table);
2243 let streaming_limit = streaming_match_recognize_limit_from_env();
2251 let source_sql = if is_streaming {
2252 format!(
2253 "SELECT * FROM {} LIMIT {}",
2254 stmt.source_table, streaming_limit
2255 )
2256 } else {
2257 format!("SELECT * FROM {}", stmt.source_table)
2258 };
2259 let source_df = self.context.sql(&source_sql).await?;
2260 let source_batches = source_df.collect().await?;
2261 if is_streaming {
2262 tracing::warn!(
2263 source = %stmt.source_table,
2264 limit = streaming_limit,
2265 collected_rows = source_batches.iter().map(|b| b.num_rows()).sum::<usize>(),
2266 "MATCH_RECOGNIZE executed against a streaming source under \
2267 bounded materialisation; results only cover the first {0} rows \
2268 of the source. Set KRISHIV_MATCH_RECOGNIZE_STREAMING_LIMIT to a \
2269 larger value if your executor has the memory budget.",
2270 streaming_limit
2271 );
2272 }
2273 let results = cep_sql::execute_match_recognize(stmt, &source_batches)?;
2274 let cep_table = next_ephemeral_name("cep_result");
2275 lakehouse::register_scan_batches(&self.context, &cep_table, results).await?;
2276 let dataframe = self
2277 .context
2278 .sql(&format!("SELECT * FROM {cep_table}"))
2279 .await?;
2280 return Ok(self.attach_query_metadata(self.make_sql_df("cep", dataframe), query));
2281 }
2282
2283 let query = &pivot_sql::rewrite_pivot_unpivot(query)?;
2286
2287 let query = &streaming_tvf::rewrite_window_tvfs(query);
2289
2290 let (rewritten, as_ofs) =
2291 lakehouse::preprocess_as_of_sql(query).unwrap_or_else(|_| (query.to_string(), vec![]));
2292 lakehouse::apply_as_of_refs(&self.context, &as_ofs).await?;
2293
2294 let can_cache = as_ofs.is_empty();
2301 let shuffle_override = self
2302 .shuffle_partitions
2303 .read()
2304 .map(|g| *g)
2305 .unwrap_or_else(|e| *e.into_inner());
2306 if can_cache {
2307 let cached_plan: Option<datafusion::logical_expr::LogicalPlan> = self
2309 .plan_cache
2310 .lock()
2311 .unwrap_or_else(|e| e.into_inner())
2312 .get(&rewritten)
2313 .cloned();
2314 if let Some(plan) = cached_plan {
2315 let dataframe = self.context.execute_logical_plan(plan).await?;
2316 return Ok(self.attach_query_metadata(
2317 self.make_sql_df("sql-query", dataframe)
2318 .with_shuffle_partitions(shuffle_override),
2319 &rewritten,
2320 ));
2321 }
2322 }
2323
2324 let dataframe = self.context.sql(&rewritten).await?;
2325
2326 if let Some(table_name) = extract_create_external_table_name(&rewritten)
2330 && !table_name.is_empty()
2331 && let Ok(provider) = self.context.table_provider(&table_name).await
2332 {
2333 let maybe_rows = provider
2334 .statistics()
2335 .and_then(|s| s.num_rows.get_value().copied());
2336 if let Some(n) = maybe_rows
2337 && let Ok(mut counts) = self.table_row_counts.write()
2338 {
2339 counts.entry(table_name).or_insert(n as u64);
2340 }
2341 }
2342
2343 if can_cache {
2345 let plan = dataframe.logical_plan().clone();
2346 match self.plan_cache.lock() {
2347 Ok(mut cache) => cache.insert(rewritten.clone(), plan),
2348 Err(poisoned) => poisoned.into_inner().insert(rewritten.clone(), plan),
2349 }
2350 }
2351
2352 Ok(self.attach_query_metadata(
2353 self.make_sql_df("sql-query", dataframe)
2354 .with_shuffle_partitions(shuffle_override),
2355 &rewritten,
2356 ))
2357 }
2358
2359 pub async fn execute_with_timeout(
2366 &self,
2367 query: impl AsRef<str> + Send,
2368 timeout_ms: u64,
2369 ) -> SqlResult<SqlDataFrame> {
2370 let timeout = std::time::Duration::from_millis(timeout_ms);
2371 tokio::time::timeout(timeout, self.sql(query))
2372 .await
2373 .map_err(|_| SqlError::Timeout { timeout_ms })?
2374 }
2375
2376 pub async fn execute_with_operation_id(
2383 &self,
2384 operation_id: u64,
2385 query: impl AsRef<str> + Send,
2386 cancelled_ids: &OperationRegistry,
2387 ) -> SqlResult<TaggedQueryResult> {
2388 if cancelled_ids.is_cancelled(operation_id) {
2389 return Err(SqlError::OperationCancelled { operation_id });
2390 }
2391 let df = self.sql(query).await?;
2392 Ok(TaggedQueryResult {
2393 operation_id,
2394 inner: df,
2395 })
2396 }
2397
2398 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2404 fn resolve_iceberg_table(
2405 &self,
2406 table_ref: &str,
2407 ) -> Option<(Arc<dyn iceberg::Catalog + Send + Sync>, iceberg::TableIdent)> {
2408 let parts: Vec<&str> = table_ref.splitn(3, '.').collect();
2409 let (catalog_arc, ns_str, table_str) = {
2410 let guard = self
2411 .iceberg_catalogs
2412 .read()
2413 .unwrap_or_else(|e| e.into_inner());
2414 if guard.is_empty() {
2415 return None;
2416 }
2417 match parts.len() {
2418 2 => {
2419 let (cat, _) = guard.first()?;
2420 (Arc::clone(cat), *parts.first()?, *parts.get(1)?)
2421 }
2422 3 => {
2423 let cat_name = parts.first().copied()?;
2424 let (cat, _) = guard.iter().find(|(_, n)| n == cat_name)?;
2425 (Arc::clone(cat), *parts.get(1)?, *parts.get(2)?)
2426 }
2427 _ => return None,
2428 }
2429 };
2430 let ns = iceberg::NamespaceIdent::from_vec(vec![ns_str.to_string()]).ok()?;
2431 let ident = iceberg::TableIdent::new(ns, table_str.to_string());
2432 Some((catalog_arc.as_iceberg(), ident))
2433 }
2434
2435 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2441 async fn execute_iceberg_ctas(
2442 &self,
2443 iceberg_catalog: Arc<dyn iceberg::Catalog + Send + Sync>,
2444 table_ident: iceberg::TableIdent,
2445 parsed_ctas: ParsedCtas,
2446 query: &str,
2447 ) -> SqlResult<SqlDataFrame> {
2448 use arrow::array::{ArrayRef, Int64Array};
2449 use arrow::datatypes::{DataType, Field, Schema};
2450 use krishiv_connectors::lakehouse::partitioned_write::parse_partition_transform;
2451
2452 let partition_by = parsed_ctas
2453 .partition_by
2454 .iter()
2455 .map(|item| parse_partition_transform(item))
2456 .collect::<Result<Vec<_>, _>>()
2457 .map_err(|e| SqlError::DataFusion {
2458 message: e.to_string(),
2459 })?;
2460
2461 let dataframe = self.context.sql(&parsed_ctas.inner_query).await?;
2462 let stream = dataframe
2463 .execute_stream()
2464 .await
2465 .map_err(|e| SqlError::DataFusion {
2466 message: e.to_string(),
2467 })?;
2468 let report = krishiv_connectors::lakehouse::dml::land_ctas(
2469 iceberg_catalog,
2470 &table_ident,
2471 parsed_ctas.or_replace,
2472 &partition_by,
2473 stream,
2474 )
2475 .await
2476 .map_err(|e| SqlError::DataFusion {
2477 message: e.to_string(),
2478 })?;
2479 self.invalidate_plan_cache();
2481 self.record_table_row_count_stat(&parsed_ctas.table_ref, report.rows as u64);
2483
2484 let schema = Arc::new(Schema::new(vec![
2485 Field::new("rows_written", DataType::Int64, false),
2486 Field::new("bytes_written", DataType::Int64, false),
2487 Field::new("data_files", DataType::Int64, false),
2488 Field::new("snapshot_id", DataType::Int64, false),
2489 ]));
2490 let columns: Vec<ArrayRef> = vec![
2491 Arc::new(Int64Array::from(vec![report.rows as i64])),
2492 Arc::new(Int64Array::from(vec![report.bytes as i64])),
2493 Arc::new(Int64Array::from(vec![report.data_files as i64])),
2494 Arc::new(Int64Array::from(vec![report.snapshot_id])),
2495 ];
2496 let batch = RecordBatch::try_new(schema, columns).map_err(|e| SqlError::DataFusion {
2497 message: e.to_string(),
2498 })?;
2499 let res_table = next_ephemeral_name("ctas_result");
2500 lakehouse::register_scan_batches(&self.context, &res_table, vec![batch]).await?;
2501 let dataframe = self
2502 .context
2503 .sql(&format!("SELECT * FROM {res_table}"))
2504 .await?;
2505 Ok(self.attach_query_metadata(self.make_sql_df("ctas", dataframe), query))
2506 }
2507
2508 fn record_table_row_count_stat(&self, table_ref: &str, row_count: u64) {
2513 let registry = krishiv_plan::optimizer::global_table_stats();
2514 let mut names = vec![table_ref];
2515 let bare = table_ref.rsplit('.').next().unwrap_or(table_ref);
2516 if bare != table_ref {
2517 names.push(bare);
2518 }
2519 for name in &names {
2520 let mut stats = registry
2521 .get(name)
2522 .unwrap_or_else(|| krishiv_plan::optimizer::TableCboStats::new(*name));
2523 stats.row_count = Some(row_count);
2524 registry.put(stats);
2525 }
2526 if let Ok(mut counts) = self.table_row_counts.write() {
2527 for name in &names {
2528 counts.insert((*name).to_owned(), row_count);
2529 }
2530 }
2531 }
2532
2533 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2538 fn adjust_table_row_count_stat(&self, table_ref: &str, delta: i64) {
2539 let registry = krishiv_plan::optimizer::global_table_stats();
2540 let mut names = vec![table_ref];
2541 let bare = table_ref.rsplit('.').next().unwrap_or(table_ref);
2542 if bare != table_ref {
2543 names.push(bare);
2544 }
2545 for name in &names {
2546 if let Some(mut stats) = registry.get(name)
2547 && let Some(current) = stats.row_count
2548 {
2549 stats.row_count = Some(current.saturating_add_signed(delta));
2550 registry.put(stats);
2551 }
2552 }
2553 if let Ok(mut counts) = self.table_row_counts.write() {
2554 for name in &names {
2555 if let Some(current) = counts.get(*name).copied() {
2556 counts.insert((*name).to_owned(), current.saturating_add_signed(delta));
2557 }
2558 }
2559 }
2560 }
2561
2562 async fn dispatch_analyze_table(&self, stmt: &str) -> SqlResult<RecordBatch> {
2573 use arrow::array::{ArrayRef, Int64Array, StringArray};
2574 use arrow::datatypes::{DataType, Field, Schema};
2575
2576 let rest = stmt
2577 .get(14..)
2578 .unwrap_or("")
2579 .trim()
2580 .trim_end_matches(';')
2581 .trim();
2582 let (table_ref, tail) = match rest.split_once(char::is_whitespace) {
2583 Some((t, tail)) => (t.trim(), tail.trim()),
2584 None => (rest, ""),
2585 };
2586 if table_ref.is_empty() {
2587 return Err(SqlError::DataFusion {
2588 message: String::from("ANALYZE TABLE: table reference is required"),
2589 });
2590 }
2591 let mut tail = tail;
2593 if tail
2594 .get(..18)
2595 .is_some_and(|p| p.eq_ignore_ascii_case("COMPUTE STATISTICS"))
2596 {
2597 tail = tail.get(18..).unwrap_or("").trim();
2598 }
2599 let columns: Vec<String> = if tail
2600 .get(..11)
2601 .is_some_and(|p| p.eq_ignore_ascii_case("FOR COLUMNS"))
2602 {
2603 tail.get(11..)
2604 .unwrap_or("")
2605 .trim()
2606 .trim_start_matches('(')
2607 .trim_end_matches(')')
2608 .split(',')
2609 .map(|c| c.trim().trim_matches('"').to_owned())
2610 .filter(|c| !c.is_empty())
2611 .collect()
2612 } else if tail.is_empty() {
2613 Vec::new()
2614 } else {
2615 return Err(SqlError::DataFusion {
2616 message: format!("ANALYZE TABLE: unexpected trailing clause: {tail}"),
2617 });
2618 };
2619
2620 let mut projections = vec![String::from("count(*)")];
2622 for c in &columns {
2623 projections.push(format!("approx_distinct(\"{c}\")"));
2624 projections.push(format!("min(\"{c}\")"));
2625 projections.push(format!("max(\"{c}\")"));
2626 projections.push(format!("count(\"{c}\")"));
2627 }
2628 let scan_sql = format!("SELECT {} FROM {table_ref}", projections.join(", "));
2629 let batches = self.context.sql(&scan_sql).await?.collect().await?;
2630 let row = batches
2631 .iter()
2632 .find(|b| b.num_rows() > 0)
2633 .ok_or_else(|| SqlError::DataFusion {
2634 message: format!("ANALYZE TABLE {table_ref}: aggregation returned no rows"),
2635 })?;
2636 let cell_string = |col: usize| -> Option<String> {
2637 let column = row.columns().get(col)?;
2638 if column.is_null(0) {
2639 return None;
2640 }
2641 arrow::util::display::array_value_to_string(column, 0).ok()
2642 };
2643 let cell_u64 = |col: usize| -> Option<u64> { cell_string(col)?.parse().ok() };
2644 let row_count = cell_u64(0).ok_or_else(|| SqlError::DataFusion {
2645 message: format!("ANALYZE TABLE {table_ref}: COUNT(*) unreadable"),
2646 })?;
2647
2648 let mut column_stats = Vec::with_capacity(columns.len());
2649 for (i, name) in columns.iter().enumerate() {
2650 let base = 1 + i * 4;
2651 let non_null = cell_u64(base + 3);
2652 column_stats.push(krishiv_plan::optimizer::ColumnCboStats {
2653 name: name.clone(),
2654 ndv: cell_u64(base),
2655 min: cell_string(base + 1),
2656 max: cell_string(base + 2),
2657 null_count: non_null.map(|n| row_count.saturating_sub(n)),
2658 });
2659 }
2660
2661 let avg_row_bytes = match self.context.table_provider(table_ref).await {
2663 Ok(provider) => provider.statistics().and_then(|s| {
2664 let rows = s.num_rows.get_value().copied()?;
2665 let bytes = s.total_byte_size.get_value().copied()?;
2666 (rows > 0).then(|| (bytes / rows) as u64)
2667 }),
2668 Err(_) => None,
2669 };
2670
2671 let mut stats = krishiv_plan::optimizer::TableCboStats::new(table_ref)
2672 .with_row_count(row_count);
2673 if let Some(bytes) = avg_row_bytes {
2674 stats = stats.with_avg_row_bytes(bytes);
2675 }
2676 if let Some(max_ndv) = column_stats.iter().filter_map(|c| c.ndv).max() {
2677 stats = stats.with_ndv(max_ndv);
2679 }
2680 stats.columns = column_stats;
2681 let registry = krishiv_plan::optimizer::global_table_stats();
2682 let bare = table_ref.rsplit('.').next().unwrap_or(table_ref);
2685 if bare != table_ref {
2686 let mut bare_stats = stats.clone();
2687 bare_stats.table = bare.to_owned();
2688 registry.put(bare_stats);
2689 }
2690 let analyzed_columns = stats.columns.len();
2691 registry.put(stats);
2692 if let Ok(mut counts) = self.table_row_counts.write() {
2693 counts.insert(table_ref.to_owned(), row_count);
2694 if bare != table_ref {
2695 counts.insert(bare.to_owned(), row_count);
2696 }
2697 }
2698 self.invalidate_plan_cache();
2699
2700 let schema = Arc::new(Schema::new(vec![
2701 Field::new("table_name", DataType::Utf8, false),
2702 Field::new("row_count", DataType::Int64, false),
2703 Field::new("avg_row_bytes", DataType::Int64, true),
2704 Field::new("columns_analyzed", DataType::Int64, false),
2705 ]));
2706 let columns_out: Vec<ArrayRef> = vec![
2707 Arc::new(StringArray::from(vec![table_ref.to_owned()])),
2708 Arc::new(Int64Array::from(vec![row_count as i64])),
2709 Arc::new(Int64Array::from(vec![avg_row_bytes.map(|b| b as i64)])),
2710 Arc::new(Int64Array::from(vec![analyzed_columns as i64])),
2711 ];
2712 RecordBatch::try_new(schema, columns_out).map_err(|e| SqlError::DataFusion {
2713 message: e.to_string(),
2714 })
2715 }
2716
2717 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2720 async fn dispatch_call_system(&self, stmt: &str) -> SqlResult<RecordBatch> {
2721 use arrow::array::{ArrayRef, Int64Array};
2722 use arrow::datatypes::{DataType, Field, Schema};
2723
2724 let upper = stmt.to_ascii_uppercase();
2725 const PREFIX: &str = "CALL SYSTEM.";
2726 let upper_after = &upper[PREFIX.len()..];
2727 let orig_after = &stmt[PREFIX.len()..];
2728
2729 let paren = upper_after.find('(').ok_or_else(|| SqlError::DataFusion {
2730 message: format!("CALL: missing '(' in: {stmt}"),
2731 })?;
2732 let proc_name = upper_after[..paren].trim();
2733
2734 let args_raw = orig_after[paren + 1..]
2735 .trim_end_matches(';')
2736 .trim()
2737 .trim_end_matches(')')
2738 .trim();
2739 let args = call_args_from_str(args_raw);
2740
2741 let iceberg_catalog = {
2742 let guard = self
2743 .iceberg_catalogs
2744 .read()
2745 .unwrap_or_else(|e| e.into_inner());
2746 guard
2747 .first()
2748 .ok_or_else(|| SqlError::DataFusion {
2749 message: "CALL system: no Iceberg catalog registered".to_string(),
2750 })?
2751 .0
2752 .as_iceberg()
2753 };
2754
2755 let table_ref = args.first().ok_or_else(|| SqlError::DataFusion {
2756 message: format!("CALL {proc_name}: table reference argument is required"),
2757 })?;
2758 let table_ident = iceberg_table_ident(table_ref)?;
2759
2760 if proc_name == "MAINTAIN_TABLE" {
2764 let older_than = parse_call_duration(args.get(1).map_or("7 days", |s| s.as_str()))?;
2765 let target_bytes = args
2766 .get(2)
2767 .and_then(|s| s.parse::<u64>().ok())
2768 .unwrap_or(128 * 1024 * 1024);
2769 let retain_last = args
2770 .get(3)
2771 .and_then(|s| s.parse::<usize>().ok())
2772 .unwrap_or(1);
2773 let report = krishiv_connectors::lakehouse::maintenance::maintain_table(
2774 iceberg_catalog,
2775 &table_ident,
2776 target_bytes,
2777 older_than,
2778 retain_last,
2779 )
2780 .await
2781 .map_err(|e| SqlError::DataFusion {
2782 message: e.to_string(),
2783 })?;
2784 let schema = Arc::new(Schema::new(vec![
2785 Field::new("compacted_files", DataType::Int64, false),
2786 Field::new("expired_snapshots", DataType::Int64, false),
2787 Field::new("removed_orphans", DataType::Int64, false),
2788 ]));
2789 let columns: Vec<ArrayRef> = vec![
2790 Arc::new(Int64Array::from(vec![report.compacted_files as i64])),
2791 Arc::new(Int64Array::from(vec![report.expired_snapshots as i64])),
2792 Arc::new(Int64Array::from(vec![report.removed_orphans as i64])),
2793 ];
2794 return RecordBatch::try_new(schema, columns).map_err(|e| SqlError::DataFusion {
2795 message: e.to_string(),
2796 });
2797 }
2798
2799 let count: i64 = match proc_name {
2800 "EXPIRE_SNAPSHOTS" => {
2801 let dur_s = args.get(1).ok_or_else(|| SqlError::DataFusion {
2802 message: "CALL expire_snapshots: duration argument is required".to_string(),
2803 })?;
2804 let older_than = parse_call_duration(dur_s)?;
2805 let retain_last = args
2806 .get(2)
2807 .and_then(|s| s.parse::<usize>().ok())
2808 .unwrap_or(1);
2809 krishiv_connectors::lakehouse::maintenance::expire_snapshots(
2810 iceberg_catalog,
2811 &table_ident,
2812 older_than,
2813 retain_last,
2814 )
2815 .await
2816 .map_err(|e| SqlError::DataFusion {
2817 message: e.to_string(),
2818 })? as i64
2819 }
2820 "REMOVE_ORPHAN_FILES" => {
2821 let dur_s = args.get(1).ok_or_else(|| SqlError::DataFusion {
2822 message: "CALL remove_orphan_files: duration argument is required".to_string(),
2823 })?;
2824 let older_than = parse_call_duration(dur_s)?;
2825 krishiv_connectors::lakehouse::maintenance::remove_orphan_files(
2826 iceberg_catalog,
2827 &table_ident,
2828 older_than,
2829 )
2830 .await
2831 .map_err(|e| SqlError::DataFusion {
2832 message: e.to_string(),
2833 })? as i64
2834 }
2835 "COMPACT_DATA_FILES" => {
2836 let target_bytes = args
2837 .get(1)
2838 .and_then(|s| s.parse::<u64>().ok())
2839 .unwrap_or(128 * 1024 * 1024);
2840 krishiv_connectors::lakehouse::maintenance::compact_data_files(
2841 iceberg_catalog,
2842 &table_ident,
2843 target_bytes,
2844 )
2845 .await
2846 .map_err(|e| SqlError::DataFusion {
2847 message: e.to_string(),
2848 })? as i64
2849 }
2850 other => {
2851 return Err(SqlError::Unsupported {
2852 feature: format!("CALL system.{other}: unknown procedure"),
2853 });
2854 }
2855 };
2856
2857 let col = match proc_name {
2858 "EXPIRE_SNAPSHOTS" => "expired_snapshots",
2859 "REMOVE_ORPHAN_FILES" => "removed_files",
2860 "COMPACT_DATA_FILES" => "rewritten_files",
2861 _ => "result",
2862 };
2863 let schema = Arc::new(Schema::new(vec![Field::new(col, DataType::Int64, false)]));
2864 let array: ArrayRef = Arc::new(Int64Array::from(vec![count]));
2865 RecordBatch::try_new(schema, vec![array]).map_err(|e| SqlError::DataFusion {
2866 message: e.to_string(),
2867 })
2868 }
2869}
2870
2871pub struct TaggedQueryResult {
2873 pub operation_id: u64,
2875 pub inner: SqlDataFrame,
2877}
2878
2879#[derive(Clone, Default)]
2885pub struct OperationRegistry {
2886 cancelled: Arc<std::sync::RwLock<std::collections::HashSet<u64>>>,
2887 progress: Arc<std::sync::RwLock<std::collections::HashMap<u64, (u64, u64)>>>,
2888}
2889
2890impl OperationRegistry {
2891 pub fn new() -> Self {
2893 Self::default()
2894 }
2895
2896 pub fn cancel(&self, operation_id: u64) {
2900 if let Ok(mut ids) = self.cancelled.write() {
2901 ids.insert(operation_id);
2902 }
2903 }
2904
2905 pub fn is_cancelled(&self, operation_id: u64) -> bool {
2907 self.cancelled
2908 .read()
2909 .map(|ids| ids.contains(&operation_id))
2910 .unwrap_or(false)
2911 }
2912
2913 pub fn remove(&self, operation_id: u64) {
2915 if let Ok(mut ids) = self.cancelled.write() {
2916 ids.remove(&operation_id);
2917 }
2918 if let Ok(mut progress) = self.progress.write() {
2919 progress.remove(&operation_id);
2920 }
2921 }
2922
2923 pub fn update_progress(&self, operation_id: u64, rows_scanned: u64, rows_emitted: u64) {
2925 if let Ok(mut progress) = self.progress.write() {
2926 progress.insert(operation_id, (rows_scanned, rows_emitted));
2927 }
2928 }
2929
2930 pub fn progress(&self, operation_id: u64) -> Option<(u64, u64)> {
2932 self.progress
2933 .read()
2934 .ok()
2935 .and_then(|progress| progress.get(&operation_id).copied())
2936 }
2937
2938 pub fn cancelled_ids(&self) -> Vec<u64> {
2940 self.cancelled
2941 .read()
2942 .map(|ids| ids.iter().copied().collect())
2943 .unwrap_or_default()
2944 }
2945}
2946
2947pub(crate) fn extract_create_external_table_name(query: &str) -> Option<String> {
2952 use datafusion::sql::parser::{DFParser, Statement as DFStatement};
2953 let mut stmts = DFParser::parse_sql(query).ok()?;
2954 match stmts.pop_front()? {
2955 DFStatement::CreateExternalTable(create) => Some(create.name.to_string()),
2956 _ => None,
2957 }
2958}
2959
2960pub enum GroupingMode<'a> {
2968 Sets(Vec<Vec<&'a krishiv_plan::expression::Expr>>),
2969 Cube(Vec<&'a krishiv_plan::expression::Expr>),
2970 Rollup(Vec<&'a krishiv_plan::expression::Expr>),
2971}
2972
2973#[async_trait::async_trait]
2974pub trait KrishivDataFrameOps: Send + Sync {
2975 async fn collect(&self) -> SqlResult<Vec<RecordBatch>>;
2977 async fn collect_with_stats(&self) -> SqlResult<(Vec<RecordBatch>, SqlExecutionStats)>;
2979 async fn explain(&self) -> SqlResult<String>;
2981 fn explain_logical(&self) -> String;
2983 fn krishiv_logical_plan(&self) -> LogicalPlan;
2985 fn query(&self) -> Option<&str>;
2987 async fn execute_stream(&self) -> SqlResult<SqlStream>;
2989
2990 fn schema(&self) -> SchemaRef;
2994
2995 async fn select(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
2997
2998 async fn select_exprs(
3000 &self,
3001 expressions: &[&krishiv_plan::expression::Expr],
3002 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3003
3004 async fn aggregate(
3006 &self,
3007 group_exprs: &[&krishiv_plan::expression::Expr],
3008 aggregate_exprs: &[&krishiv_plan::expression::Expr],
3009 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3010
3011 async fn aggregate_grouping(
3013 &self,
3014 grouping: GroupingMode<'_>,
3015 aggregate_exprs: &[&krishiv_plan::expression::Expr],
3016 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3017
3018 async fn pivot(
3020 &self,
3021 group_exprs: &[&krishiv_plan::expression::Expr],
3022 pivot_column: &krishiv_plan::expression::Expr,
3023 aggregate_expr: &krishiv_plan::expression::Expr,
3024 values: &[(krishiv_plan::expression::ScalarValue, String)],
3025 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3026
3027 async fn unpivot(
3029 &self,
3030 columns: &[&str],
3031 name_column: &str,
3032 value_column: &str,
3033 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3034
3035 async fn filter(&self, predicate: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3037
3038 async fn filter_expr(
3040 &self,
3041 predicate: &krishiv_plan::expression::Expr,
3042 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3043
3044 async fn limit(&self, n: usize) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3046
3047 async fn distinct(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3049
3050 async fn drop_nulls(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3052
3053 async fn sample(&self, fraction: f64) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3055
3056 async fn sort(
3058 &self,
3059 columns: &[&str],
3060 descending: &[bool],
3061 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3062
3063 async fn alias(&self, alias: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3065
3066 async fn drop_columns(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3068
3069 async fn rename_column(&self, old: &str, new: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3071
3072 async fn with_column(&self, name: &str, expr: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3074
3075 fn as_any(&self) -> &dyn std::any::Any;
3077
3078 async fn describe(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3080
3081 async fn fill_null(&self, column: &str, value: &str)
3083 -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3084
3085 async fn join(
3087 &self,
3088 right: &dyn KrishivDataFrameOps,
3089 how: &str,
3090 left_on: &[&str],
3091 right_on: &[&str],
3092 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3093
3094 async fn union(
3096 &self,
3097 right: &dyn KrishivDataFrameOps,
3098 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3099
3100 async fn union_distinct(
3101 &self,
3102 right: &dyn KrishivDataFrameOps,
3103 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3104
3105 async fn intersect(
3106 &self,
3107 right: &dyn KrishivDataFrameOps,
3108 distinct: bool,
3109 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3110
3111 async fn except(
3112 &self,
3113 right: &dyn KrishivDataFrameOps,
3114 distinct: bool,
3115 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3116
3117 async fn register_batches(&self, name: &str, batches: Vec<RecordBatch>) -> SqlResult<()>;
3120
3121 async fn deregister_table(&self, name: &str) -> SqlResult<()>;
3123
3124 async fn create_view(&self, name: &str, replace: bool) -> SqlResult<()>;
3127}
3128
3129fn df_plan_to_krishiv_nodes(
3137 plan: &datafusion::logical_expr::LogicalPlan,
3138 table_row_counts: &std::collections::HashMap<String, u64>,
3139 counter: &mut usize,
3140) -> (Vec<krishiv_plan::PlanNode>, String) {
3141 use datafusion::logical_expr::LogicalPlan as DfPlan;
3142 use krishiv_plan::{ExecutionKind, NodeOp, PlanNode};
3143
3144 *counter += 1;
3145 let idx = *counter;
3146
3147 match plan {
3148 DfPlan::TableScan(ts) => {
3149 let table_name = ts.table_name.table().to_string();
3150 let row_count = table_row_counts.get(&table_name).copied();
3151 let filters: Vec<String> = ts.filters.iter().map(|e| e.to_string()).collect();
3152 let id = format!("scan-{idx}");
3153 let node = PlanNode::new(&id, format!("Scan {table_name}"), ExecutionKind::Batch)
3154 .with_op(NodeOp::Scan {
3155 table: table_name,
3156 filters,
3157 })
3158 .with_estimated_rows(row_count);
3159 (vec![node], id)
3160 }
3161
3162 DfPlan::Projection(proj) => {
3163 let (mut nodes, input_id) =
3164 df_plan_to_krishiv_nodes(&proj.input, table_row_counts, counter);
3165 let id = format!("proj-{idx}");
3166 let columns: Vec<String> = proj.expr.iter().map(|e| e.to_string()).collect();
3167 nodes.push(
3168 PlanNode::new(&id, "Projection", ExecutionKind::Batch)
3169 .with_op(NodeOp::Project { columns })
3170 .with_inputs([input_id]),
3171 );
3172 (nodes, id)
3173 }
3174
3175 DfPlan::Filter(filter) => {
3176 let (mut nodes, input_id) =
3177 df_plan_to_krishiv_nodes(&filter.input, table_row_counts, counter);
3178 let id = format!("filter-{idx}");
3179 let predicate = filter.predicate.to_string();
3180 nodes.push(
3181 PlanNode::new(&id, "Filter", ExecutionKind::Batch)
3182 .with_op(NodeOp::Filter { predicate })
3183 .with_inputs([input_id]),
3184 );
3185 (nodes, id)
3186 }
3187
3188 DfPlan::Aggregate(agg) => {
3189 let (mut nodes, input_id) =
3190 df_plan_to_krishiv_nodes(&agg.input, table_row_counts, counter);
3191 let id = format!("agg-{idx}");
3192 let group_keys: Vec<String> = agg.group_expr.iter().map(|e| e.to_string()).collect();
3193 nodes.push(
3194 PlanNode::new(&id, "Aggregate", ExecutionKind::Batch)
3195 .with_op(NodeOp::Aggregate { group_keys })
3196 .with_inputs([input_id]),
3197 );
3198 (nodes, id)
3199 }
3200
3201 DfPlan::Join(join) => {
3202 let (mut nodes, left_id) =
3203 df_plan_to_krishiv_nodes(&join.left, table_row_counts, counter);
3204 let (right_nodes, right_id) =
3205 df_plan_to_krishiv_nodes(&join.right, table_row_counts, counter);
3206 nodes.extend(right_nodes);
3207 let id = format!("join-{idx}");
3208 let krishiv_join_type = match join.join_type {
3213 datafusion::common::JoinType::Inner => krishiv_plan::JoinType::Inner,
3214 datafusion::common::JoinType::Left => krishiv_plan::JoinType::Left,
3215 datafusion::common::JoinType::Right => krishiv_plan::JoinType::Right,
3216 datafusion::common::JoinType::Full => krishiv_plan::JoinType::Full,
3217 datafusion::common::JoinType::LeftSemi => krishiv_plan::JoinType::LeftSemi,
3218 datafusion::common::JoinType::RightSemi => krishiv_plan::JoinType::RightSemi,
3219 datafusion::common::JoinType::LeftAnti => krishiv_plan::JoinType::LeftAnti,
3220 datafusion::common::JoinType::RightAnti => krishiv_plan::JoinType::RightAnti,
3221 datafusion::common::JoinType::LeftMark => krishiv_plan::JoinType::LeftSemi,
3225 datafusion::common::JoinType::RightMark => krishiv_plan::JoinType::RightSemi,
3226 };
3227 nodes.push(
3228 PlanNode::new(&id, "Join", ExecutionKind::Batch)
3229 .with_op(NodeOp::Join {
3230 join_type: krishiv_join_type,
3231 })
3232 .with_inputs([left_id, right_id]),
3233 );
3234 (nodes, id)
3235 }
3236
3237 DfPlan::Sort(sort) => {
3238 let (mut nodes, input_id) =
3239 df_plan_to_krishiv_nodes(&sort.input, table_row_counts, counter);
3240 let id = format!("sort-{idx}");
3241 nodes.push(
3242 PlanNode::new(&id, "Sort", ExecutionKind::Batch)
3243 .with_op(NodeOp::Other {
3244 description: format!(
3245 "Sort({})",
3246 sort.expr
3247 .iter()
3248 .map(|e| e.to_string())
3249 .collect::<Vec<_>>()
3250 .join(", ")
3251 ),
3252 })
3253 .with_inputs([input_id]),
3254 );
3255 (nodes, id)
3256 }
3257
3258 DfPlan::Repartition(repart) => {
3259 let (mut nodes, input_id) =
3260 df_plan_to_krishiv_nodes(&repart.input, table_row_counts, counter);
3261 let id = format!("exchange-{idx}");
3262 let partitioning = krishiv_plan::Partitioning::Unpartitioned;
3263 nodes.push(
3264 PlanNode::new(&id, "Exchange", ExecutionKind::Batch)
3265 .with_op(NodeOp::Exchange { partitioning })
3266 .with_inputs([input_id]),
3267 );
3268 (nodes, id)
3269 }
3270
3271 DfPlan::Limit(limit) => {
3272 let (mut nodes, input_id) =
3273 df_plan_to_krishiv_nodes(&limit.input, table_row_counts, counter);
3274 let id = format!("limit-{idx}");
3275 nodes.push(
3276 PlanNode::new(&id, "Limit", ExecutionKind::Batch)
3277 .with_op(NodeOp::Other {
3278 description: format!(
3279 "Limit(skip={:?}, fetch={:?})",
3280 limit.skip.as_ref().map(|e| e.to_string()),
3281 limit.fetch.as_ref().map(|e| e.to_string()),
3282 ),
3283 })
3284 .with_inputs([input_id]),
3285 );
3286 (nodes, id)
3287 }
3288
3289 DfPlan::Union(union) if union.inputs.len() == 1 => {
3290 if let Some(input) = union.inputs.first() {
3291 df_plan_to_krishiv_nodes(input, table_row_counts, counter)
3292 } else {
3293 (Vec::new(), String::new())
3294 }
3295 }
3296 DfPlan::Union(union) => {
3297 let mut all_nodes = Vec::new();
3298 let mut input_ids = Vec::new();
3299 for input in &union.inputs {
3300 let (sub_nodes, sub_id) =
3301 df_plan_to_krishiv_nodes(input, table_row_counts, counter);
3302 all_nodes.extend(sub_nodes);
3303 input_ids.push(sub_id);
3304 }
3305 let id = format!("union-{idx}");
3306 all_nodes.push(
3307 PlanNode::new(&id, "Union", ExecutionKind::Batch)
3308 .with_op(NodeOp::Other {
3309 description: "Union".to_string(),
3310 })
3311 .with_inputs(input_ids),
3312 );
3313 (all_nodes, id)
3314 }
3315
3316 DfPlan::SubqueryAlias(alias) => {
3317 df_plan_to_krishiv_nodes(&alias.input, table_row_counts, counter)
3319 }
3320
3321 DfPlan::Values(_) => {
3322 let id = format!("values-{idx}");
3323 let node = PlanNode::new(&id, "Values", ExecutionKind::Batch).with_op(NodeOp::Other {
3324 description: "Values".to_string(),
3325 });
3326 (vec![node], id)
3327 }
3328
3329 DfPlan::Extension(_) => {
3330 let id = format!("ext-{idx}");
3331 let label = plan.to_string();
3332 let node = PlanNode::new(&id, label.clone(), ExecutionKind::Batch)
3333 .with_op(NodeOp::Other { description: label });
3334 (vec![node], id)
3335 }
3336
3337 DfPlan::EmptyRelation(_) => {
3338 let id = format!("empty-{idx}");
3339 let node =
3340 PlanNode::new(&id, "EmptyRelation", ExecutionKind::Batch).with_op(NodeOp::Other {
3341 description: "EmptyRelation".to_string(),
3342 });
3343 (vec![node], id)
3344 }
3345
3346 _ => {
3348 let id = format!("df-{idx}");
3349 let label = plan.to_string();
3350 let node = PlanNode::new(&id, label.clone(), ExecutionKind::Batch)
3351 .with_op(NodeOp::Other { description: label });
3352 (vec![node], id)
3353 }
3354 }
3355}
3356
3357#[derive(Clone)]
3359pub struct SqlDataFrame {
3360 name: String,
3361 query: Option<String>,
3362 query_text: Option<String>,
3364 execution_kind: ExecutionKind,
3365 dataframe: DataFusionDataFrame,
3366 shuffle_partitions: Option<u32>,
3367 context: SessionContext,
3369 table_row_counts: Arc<std::sync::RwLock<HashMap<String, u64>>>,
3373}
3374
3375impl fmt::Debug for SqlDataFrame {
3376 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3377 f.debug_struct("SqlDataFrame")
3378 .field("name", &self.name)
3379 .field("query", &self.query)
3380 .field("shuffle_partitions", &self.shuffle_partitions)
3381 .finish_non_exhaustive()
3382 }
3383}
3384
3385impl SqlDataFrame {
3386 fn new(
3387 name: impl Into<String>,
3388 dataframe: DataFusionDataFrame,
3389 table_row_counts: Arc<std::sync::RwLock<HashMap<String, u64>>>,
3390 ) -> Self {
3391 Self {
3392 name: name.into(),
3393 query: None,
3394 query_text: None,
3395 execution_kind: ExecutionKind::Batch,
3396 dataframe,
3397 shuffle_partitions: None,
3398 context: SessionContext::default(),
3399 table_row_counts,
3400 }
3401 }
3402
3403 pub(crate) fn with_context(mut self, context: SessionContext) -> Self {
3405 self.context = context;
3406 self
3407 }
3408
3409 fn with_query(mut self, query: impl Into<String>) -> Self {
3410 let q = query.into();
3411 self.query_text = Some(q.clone());
3412 self.query = Some(q);
3413 self
3414 }
3415
3416 fn with_execution_kind(mut self, kind: ExecutionKind) -> Self {
3417 self.execution_kind = kind;
3418 self
3419 }
3420
3421 fn with_shuffle_partitions(mut self, n: Option<u32>) -> Self {
3422 self.shuffle_partitions = n;
3423 self
3424 }
3425
3426 pub fn query(&self) -> Option<&str> {
3428 self.query.as_deref()
3429 }
3430
3431 pub fn arrow_schema(&self) -> arrow::datatypes::SchemaRef {
3437 std::sync::Arc::new(self.dataframe.schema().as_arrow().clone())
3438 }
3439
3440 fn with_new_dataframe(&self, df: DataFusionDataFrame, tag: &str) -> Self {
3444 Self {
3445 name: format!("{}-{}", self.name, tag),
3446 query: None,
3447 query_text: None,
3448 execution_kind: self.execution_kind,
3449 dataframe: df,
3450 shuffle_partitions: self.shuffle_partitions,
3451 context: self.context.clone(),
3452 table_row_counts: self.table_row_counts.clone(),
3453 }
3454 }
3455
3456 pub fn krishiv_logical_plan(&self) -> LogicalPlan {
3465 let df_plan = self.dataframe.logical_plan();
3466 let counts = self
3467 .table_row_counts
3468 .read()
3469 .unwrap_or_else(|e| e.into_inner());
3470 let mut counter = 0usize;
3471 let (nodes, _root_id) = df_plan_to_krishiv_nodes(df_plan, &counts, &mut counter);
3472
3473 let mut plan = LogicalPlan::new(self.name.clone(), self.execution_kind);
3474 for node in nodes {
3475 plan = plan.with_node(node);
3476 }
3477
3478 let optimizer = krishiv_plan::optimizer::default_logical_optimizer();
3483 let fallback = plan.clone();
3484 match optimizer.optimize(plan) {
3485 Ok(result) => result.plan,
3486 Err(error) => {
3487 tracing::warn!(
3488 plan = %self.name,
3489 %error,
3490 "logical optimizer failed; using unoptimized plan"
3491 );
3492 fallback
3493 }
3494 }
3495 }
3496
3497 pub fn explain_logical(&self) -> String {
3499 self.dataframe.logical_plan().to_string()
3500 }
3501
3502 pub async fn explain(&self) -> SqlResult<String> {
3504 let batches = self
3505 .dataframe
3506 .clone()
3507 .explain(false, false)?
3508 .collect()
3509 .await?;
3510 pretty_batches(&batches)
3511 }
3512
3513 pub async fn collect(&self) -> SqlResult<Vec<RecordBatch>> {
3515 Ok(self.dataframe.clone().collect().await?)
3516 }
3517
3518 pub async fn execute_stream(&self) -> SqlResult<SqlStream> {
3520 let df_stream = self.dataframe.clone().execute_stream().await?;
3521 use futures::StreamExt;
3522 let mapped = df_stream.map(|res| {
3523 res.map_err(|e| SqlError::DataFusion {
3524 message: e.to_string(),
3525 })
3526 });
3527 Ok(Box::pin(mapped))
3528 }
3529
3530 pub async fn collect_with_stats(&self) -> SqlResult<(Vec<RecordBatch>, SqlExecutionStats)> {
3538 use datafusion::physical_plan::collect as df_collect;
3539
3540 let df = self.dataframe.clone();
3541 let task_ctx = df.task_ctx();
3542 let physical_plan = df.create_physical_plan().await?;
3543
3544 let batches = df_collect(physical_plan.clone(), task_ctx.into()).await?;
3545
3546 let mut output_rows: u64 = batches.iter().map(|b| b.num_rows() as u64).sum();
3547 let mut cpu_nanos: u64 = 0;
3548
3549 if let Some(metrics) = physical_plan.metrics() {
3550 if let Some(v) = metrics.output_rows() {
3551 output_rows = v as u64;
3552 }
3553 if let Some(t) = metrics.elapsed_compute() {
3554 cpu_nanos = t as u64;
3555 }
3556 }
3557
3558 let (spill_bytes, spill_count) = aggregate_spill_metrics(physical_plan.as_ref());
3559
3560 Ok((
3561 batches,
3562 SqlExecutionStats {
3563 output_rows,
3564 cpu_nanos,
3565 spill_bytes,
3566 spill_count,
3567 },
3568 ))
3569 }
3570
3571 pub async fn execute_stream_with_stats(&self) -> SqlResult<(SqlStream, SqlStatsHandle)> {
3581 use futures::StreamExt;
3582
3583 let df = self.dataframe.clone();
3584 let task_ctx = df.task_ctx();
3585 let physical_plan = df.create_physical_plan().await?;
3586 let df_stream = datafusion::physical_plan::execute_stream(
3587 physical_plan.clone(),
3588 std::sync::Arc::new(task_ctx),
3589 )?;
3590 let mapped = df_stream.map(|res| {
3591 res.map_err(|e| SqlError::DataFusion {
3592 message: e.to_string(),
3593 })
3594 });
3595 Ok((
3596 Box::pin(mapped),
3597 SqlStatsHandle {
3598 plan: physical_plan,
3599 },
3600 ))
3601 }
3602}
3603
3604pub struct SqlStatsHandle {
3607 plan: std::sync::Arc<dyn datafusion::physical_plan::ExecutionPlan>,
3608}
3609
3610impl SqlStatsHandle {
3611 pub fn stats(&self) -> SqlExecutionStats {
3616 let mut output_rows: u64 = 0;
3617 let mut cpu_nanos: u64 = 0;
3618 if let Some(metrics) = self.plan.metrics() {
3619 if let Some(v) = metrics.output_rows() {
3620 output_rows = v as u64;
3621 }
3622 if let Some(t) = metrics.elapsed_compute() {
3623 cpu_nanos = t as u64;
3624 }
3625 }
3626 let (spill_bytes, spill_count) = aggregate_spill_metrics(self.plan.as_ref());
3627 SqlExecutionStats {
3628 output_rows,
3629 cpu_nanos,
3630 spill_bytes,
3631 spill_count,
3632 }
3633 }
3634}
3635
3636fn aggregate_spill_metrics(plan: &dyn datafusion::physical_plan::ExecutionPlan) -> (u64, u64) {
3643 let mut spill_bytes: u64 = 0;
3644 let mut spill_count: u64 = 0;
3645 if let Some(metrics) = plan.metrics() {
3646 if let Some(bytes) = metrics.spilled_bytes() {
3647 spill_bytes = spill_bytes.saturating_add(bytes as u64);
3648 }
3649 if let Some(count) = metrics.spill_count() {
3650 spill_count = spill_count.saturating_add(count as u64);
3651 }
3652 }
3653 for child in plan.children() {
3654 let (child_bytes, child_count) = aggregate_spill_metrics(child.as_ref());
3655 spill_bytes = spill_bytes.saturating_add(child_bytes);
3656 spill_count = spill_count.saturating_add(child_count);
3657 }
3658 (spill_bytes, spill_count)
3659}
3660
3661#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3663pub struct SqlExecutionStats {
3664 pub output_rows: u64,
3665 pub cpu_nanos: u64,
3666 pub spill_bytes: u64,
3668 pub spill_count: u64,
3670}
3671
3672fn top_level_alias_index(expression: &str) -> Option<usize> {
3673 let bytes = expression.as_bytes();
3674 let mut depth = 0usize;
3675 let mut single_quoted = false;
3676 let mut double_quoted = false;
3677 let mut candidate = None;
3678 let mut index = 0usize;
3679 while index < bytes.len() {
3680 let Some(&byte) = bytes.get(index) else {
3681 break;
3682 };
3683 match byte {
3684 b'\'' if !double_quoted => {
3685 if single_quoted && bytes.get(index + 1) == Some(&b'\'') {
3686 index += 2;
3687 continue;
3688 }
3689 single_quoted = !single_quoted;
3690 }
3691 b'"' if !single_quoted => {
3692 if double_quoted && bytes.get(index + 1) == Some(&b'"') {
3693 index += 2;
3694 continue;
3695 }
3696 double_quoted = !double_quoted;
3697 }
3698 b'(' if !single_quoted && !double_quoted => depth += 1,
3699 b')' if !single_quoted && !double_quoted => depth = depth.saturating_sub(1),
3700 b' ' if depth == 0
3701 && !single_quoted
3702 && !double_quoted
3703 && bytes
3704 .get(index..index + 4)
3705 .is_some_and(|slice| slice.eq_ignore_ascii_case(b" AS ")) =>
3706 {
3707 candidate = Some(index);
3708 index += 3;
3709 }
3710 _ => {}
3711 }
3712 index += 1;
3713 }
3714 candidate
3715}
3716
3717fn parse_dataframe_expression(
3718 dataframe: &datafusion::dataframe::DataFrame,
3719 expression: &str,
3720) -> SqlResult<datafusion::logical_expr::Expr> {
3721 if let Some(index) = top_level_alias_index(expression) {
3722 let (body, alias) = expression.split_at(index);
3723 let alias = alias[4..].trim();
3724 if !alias.is_empty() {
3725 let alias = alias
3726 .strip_prefix('"')
3727 .and_then(|value| value.strip_suffix('"'))
3728 .unwrap_or(alias)
3729 .replace("\"\"", "\"");
3730 return Ok(dataframe.parse_sql_expr(body.trim())?.alias(alias));
3731 }
3732 }
3733 dataframe.parse_sql_expr(expression).map_err(Into::into)
3734}
3735
3736pub fn parse_public_expression(sql: &str) -> SqlResult<krishiv_plan::expression::Expr> {
3738 let dialect = GenericDialect {};
3739 let mut parser =
3740 Parser::new(&dialect)
3741 .try_with_sql(sql)
3742 .map_err(|error| SqlError::Unsupported {
3743 feature: format!("public expression parse: {error}"),
3744 })?;
3745 let expression = parser.parse_expr().map_err(|error| SqlError::Unsupported {
3746 feature: format!("public expression parse: {error}"),
3747 })?;
3748 sqlparser_expression_to_public(&expression)
3749}
3750
3751fn sqlparser_expression_to_public(
3752 expression: &datafusion::sql::sqlparser::ast::Expr,
3753) -> SqlResult<krishiv_plan::expression::Expr> {
3754 use datafusion::sql::sqlparser::ast::{BinaryOperator as SqlOperator, Expr as SqlExpr, Value};
3755 use krishiv_plan::expression::{BinaryOperator, Expr, ScalarValue};
3756
3757 Ok(match expression {
3758 SqlExpr::Identifier(identifier) => Expr::Column {
3759 path: vec![identifier.value.clone()],
3760 },
3761 SqlExpr::CompoundIdentifier(identifiers) => Expr::Column {
3762 path: identifiers
3763 .iter()
3764 .map(|identifier| identifier.value.clone())
3765 .collect(),
3766 },
3767 SqlExpr::Nested(expression) => sqlparser_expression_to_public(expression)?,
3768 SqlExpr::IsNull(expression) => Expr::IsNull {
3769 expression: Box::new(sqlparser_expression_to_public(expression)?),
3770 negated: false,
3771 },
3772 SqlExpr::IsNotNull(expression) => Expr::IsNull {
3773 expression: Box::new(sqlparser_expression_to_public(expression)?),
3774 negated: true,
3775 },
3776 SqlExpr::BinaryOp { left, op, right } => Expr::Binary {
3777 left: Box::new(sqlparser_expression_to_public(left)?),
3778 op: match op {
3779 SqlOperator::Eq => BinaryOperator::Eq,
3780 SqlOperator::NotEq => BinaryOperator::NotEq,
3781 SqlOperator::Gt => BinaryOperator::Gt,
3782 SqlOperator::GtEq => BinaryOperator::GtEq,
3783 SqlOperator::Lt => BinaryOperator::Lt,
3784 SqlOperator::LtEq => BinaryOperator::LtEq,
3785 SqlOperator::And => BinaryOperator::And,
3786 SqlOperator::Or => BinaryOperator::Or,
3787 SqlOperator::Plus => BinaryOperator::Plus,
3788 SqlOperator::Minus => BinaryOperator::Minus,
3789 SqlOperator::Multiply => BinaryOperator::Multiply,
3790 SqlOperator::Divide => BinaryOperator::Divide,
3791 other => {
3792 return Err(SqlError::Unsupported {
3793 feature: format!("public expression operator {other}"),
3794 });
3795 }
3796 },
3797 right: Box::new(sqlparser_expression_to_public(right)?),
3798 },
3799 SqlExpr::Value(value) => Expr::Literal {
3800 value: match &value.value {
3801 Value::Null => ScalarValue::Null,
3802 Value::Boolean(value) => ScalarValue::Boolean(*value),
3803 Value::SingleQuotedString(value) => ScalarValue::Utf8(value.clone()),
3804 Value::Number(value, _)
3805 if value.contains('.') || value.contains('e') || value.contains('E') =>
3806 {
3807 ScalarValue::float64(value.parse::<f64>().map_err(|error| {
3808 SqlError::Unsupported {
3809 feature: format!("numeric expression literal: {error}"),
3810 }
3811 })?)
3812 }
3813 Value::Number(value, _) => {
3814 ScalarValue::Int64(value.parse::<i64>().map_err(|error| {
3815 SqlError::Unsupported {
3816 feature: format!("integer expression literal: {error}"),
3817 }
3818 })?)
3819 }
3820 other => {
3821 return Err(SqlError::Unsupported {
3822 feature: format!("public expression literal {other}"),
3823 });
3824 }
3825 },
3826 },
3827 other => {
3828 return Err(SqlError::Unsupported {
3829 feature: format!("public expression node {other}"),
3830 });
3831 }
3832 })
3833}
3834
3835fn public_data_type_to_arrow(
3836 data_type: &krishiv_plan::expression::ExprDataType,
3837) -> arrow::datatypes::DataType {
3838 use arrow::datatypes::{DataType, Field, IntervalUnit, TimeUnit};
3839 use krishiv_plan::expression::{ExprDataType, IntervalUnit as PublicIntervalUnit};
3840
3841 match data_type {
3842 ExprDataType::Null => DataType::Null,
3843 ExprDataType::Boolean => DataType::Boolean,
3844 ExprDataType::Int64 => DataType::Int64,
3845 ExprDataType::UInt64 => DataType::UInt64,
3846 ExprDataType::Float64 => DataType::Float64,
3847 ExprDataType::Utf8 => DataType::Utf8,
3848 ExprDataType::Binary => DataType::Binary,
3849 ExprDataType::Decimal128 { precision, scale } => DataType::Decimal128(*precision, *scale),
3850 ExprDataType::Date32 => DataType::Date32,
3851 ExprDataType::Timestamp { unit, timezone } => DataType::Timestamp(
3852 match unit {
3853 krishiv_plan::expression::TimeUnit::Second => TimeUnit::Second,
3854 krishiv_plan::expression::TimeUnit::Millisecond => TimeUnit::Millisecond,
3855 krishiv_plan::expression::TimeUnit::Microsecond => TimeUnit::Microsecond,
3856 krishiv_plan::expression::TimeUnit::Nanosecond => TimeUnit::Nanosecond,
3857 },
3858 timezone.clone().map(Into::into),
3859 ),
3860 ExprDataType::Interval { unit } => DataType::Interval(match unit {
3861 PublicIntervalUnit::YearMonth => IntervalUnit::YearMonth,
3862 PublicIntervalUnit::DayTime => IntervalUnit::DayTime,
3863 PublicIntervalUnit::MonthDayNano => IntervalUnit::MonthDayNano,
3864 }),
3865 ExprDataType::List(element) => DataType::List(Arc::new(Field::new(
3866 "item",
3867 public_data_type_to_arrow(element),
3868 true,
3869 ))),
3870 ExprDataType::Map { key, value } => DataType::Map(
3871 Arc::new(Field::new(
3872 "entries",
3873 DataType::Struct(
3874 vec![
3875 Arc::new(Field::new("key", public_data_type_to_arrow(key), false)),
3876 Arc::new(Field::new("value", public_data_type_to_arrow(value), true)),
3877 ]
3878 .into(),
3879 ),
3880 false,
3881 )),
3882 false,
3883 ),
3884 ExprDataType::Struct(fields) => DataType::Struct(
3885 fields
3886 .iter()
3887 .map(|field| {
3888 Arc::new(Field::new(
3889 &field.name,
3890 public_data_type_to_arrow(&field.data_type),
3891 field.nullable,
3892 ))
3893 })
3894 .collect::<Vec<_>>()
3895 .into(),
3896 ),
3897 ExprDataType::Variant => DataType::Utf8,
3902 }
3903}
3904
3905fn public_scalar_to_datafusion(
3906 value: &krishiv_plan::expression::ScalarValue,
3907) -> Option<datafusion::common::ScalarValue> {
3908 use datafusion::common::ScalarValue;
3909 use krishiv_plan::expression::{ScalarValue as PublicScalar, TimeUnit};
3910
3911 Some(match value {
3912 PublicScalar::Null => ScalarValue::Null,
3913 PublicScalar::Boolean(value) => ScalarValue::Boolean(Some(*value)),
3914 PublicScalar::Int64(value) => ScalarValue::Int64(Some(*value)),
3915 PublicScalar::UInt64(value) => ScalarValue::UInt64(Some(*value)),
3916 PublicScalar::Float64(bits) => ScalarValue::Float64(Some(f64::from_bits(*bits))),
3917 PublicScalar::Utf8(value) => ScalarValue::Utf8(Some(value.clone())),
3918 PublicScalar::Binary(value) => ScalarValue::Binary(Some(value.clone())),
3919 PublicScalar::Decimal128 {
3920 value,
3921 precision,
3922 scale,
3923 } => ScalarValue::Decimal128(Some(*value), *precision, *scale),
3924 PublicScalar::Date32(value) => ScalarValue::Date32(Some(*value)),
3925 PublicScalar::Timestamp {
3926 value,
3927 unit,
3928 timezone,
3929 } => {
3930 let timezone = timezone.clone().map(Into::into);
3931 match unit {
3932 TimeUnit::Second => ScalarValue::TimestampSecond(Some(*value), timezone),
3933 TimeUnit::Millisecond => ScalarValue::TimestampMillisecond(Some(*value), timezone),
3934 TimeUnit::Microsecond => ScalarValue::TimestampMicrosecond(Some(*value), timezone),
3935 TimeUnit::Nanosecond => ScalarValue::TimestampNanosecond(Some(*value), timezone),
3936 }
3937 }
3938 PublicScalar::Interval { .. } => return None,
3939 })
3940}
3941
3942fn lower_public_expression(
3948 dataframe: &datafusion::dataframe::DataFrame,
3949 expression: &krishiv_plan::expression::Expr,
3950) -> SqlResult<datafusion::logical_expr::Expr> {
3951 expression
3952 .validate()
3953 .map_err(|error| SqlError::Unsupported {
3954 feature: format!("invalid public expression: {error}"),
3955 })?;
3956 use datafusion::logical_expr::{Expr as DataFusionExpr, Operator, binary_expr, cast, try_cast};
3957 use krishiv_plan::expression::{BinaryOperator, Expr};
3958
3959 Ok(match expression {
3960 Expr::Column { path } if path.len() == 1 => {
3961 datafusion::prelude::col(path.first().map(String::as_str).unwrap_or(""))
3962 }
3963 Expr::Column { .. } => parse_dataframe_expression(dataframe, &expression.to_sql())?,
3964 Expr::Literal { value } => match public_scalar_to_datafusion(value) {
3965 Some(value) => DataFusionExpr::Literal(value, None),
3966 None => parse_dataframe_expression(dataframe, &expression.to_sql())?,
3967 },
3968 Expr::Alias { expression, name } => {
3969 lower_public_expression(dataframe, expression)?.alias(name)
3970 }
3971 Expr::Binary { left, op, right } => binary_expr(
3972 lower_public_expression(dataframe, left)?,
3973 match op {
3974 BinaryOperator::Eq => Operator::Eq,
3975 BinaryOperator::NotEq => Operator::NotEq,
3976 BinaryOperator::Gt => Operator::Gt,
3977 BinaryOperator::GtEq => Operator::GtEq,
3978 BinaryOperator::Lt => Operator::Lt,
3979 BinaryOperator::LtEq => Operator::LtEq,
3980 BinaryOperator::And => Operator::And,
3981 BinaryOperator::Or => Operator::Or,
3982 BinaryOperator::Plus => Operator::Plus,
3983 BinaryOperator::Minus => Operator::Minus,
3984 BinaryOperator::Multiply => Operator::Multiply,
3985 BinaryOperator::Divide => Operator::Divide,
3986 },
3987 lower_public_expression(dataframe, right)?,
3988 ),
3989 Expr::IsNull {
3990 expression,
3991 negated,
3992 } => {
3993 let expression = lower_public_expression(dataframe, expression)?;
3994 if *negated {
3995 expression.is_not_null()
3996 } else {
3997 expression.is_null()
3998 }
3999 }
4000 Expr::Cast {
4001 expression,
4002 data_type,
4003 safe,
4004 } => {
4005 let expression = lower_public_expression(dataframe, expression)?;
4006 let data_type = public_data_type_to_arrow(data_type);
4007 if *safe {
4008 try_cast(expression, data_type)
4009 } else {
4010 cast(expression, data_type)
4011 }
4012 }
4013 Expr::Sort { .. } => {
4014 return Err(SqlError::Unsupported {
4015 feature: "standalone sort expressions are only valid inside windows or order_by"
4016 .into(),
4017 });
4018 }
4019 Expr::Aggregate { .. }
4020 | Expr::Function { .. }
4021 | Expr::Window { .. }
4022 | Expr::RawSql { .. } => parse_dataframe_expression(dataframe, &expression.to_sql())?,
4023 })
4024}
4025
4026fn sql_dataframe<'a>(
4027 dataframe: &'a dyn KrishivDataFrameOps,
4028 operation: &str,
4029) -> SqlResult<&'a SqlDataFrame> {
4030 dataframe
4031 .as_any()
4032 .downcast_ref::<SqlDataFrame>()
4033 .ok_or_else(|| SqlError::DataFusion {
4034 message: format!("right DataFrame must be SqlDataFrame for {operation}"),
4035 })
4036}
4037
4038#[async_trait::async_trait]
4039impl KrishivDataFrameOps for SqlDataFrame {
4040 async fn collect(&self) -> SqlResult<Vec<RecordBatch>> {
4041 SqlDataFrame::collect(self).await
4042 }
4043 async fn collect_with_stats(&self) -> SqlResult<(Vec<RecordBatch>, SqlExecutionStats)> {
4044 SqlDataFrame::collect_with_stats(self).await
4045 }
4046 async fn explain(&self) -> SqlResult<String> {
4047 SqlDataFrame::explain(self).await
4048 }
4049 fn explain_logical(&self) -> String {
4050 SqlDataFrame::explain_logical(self)
4051 }
4052 fn krishiv_logical_plan(&self) -> LogicalPlan {
4053 let label = self.dataframe.logical_plan().to_string();
4054 let mut plan = LogicalPlan::new(self.name.clone(), ExecutionKind::Batch).with_node(
4055 PlanNode::new("datafusion-logical", label, ExecutionKind::Batch),
4056 );
4057 if let Some(n) = self.shuffle_partitions {
4058 plan = plan.with_shuffle_partitions(Some(n));
4059 }
4060 plan
4061 }
4062 fn query(&self) -> Option<&str> {
4063 SqlDataFrame::query(self)
4064 }
4065 async fn execute_stream(&self) -> SqlResult<SqlStream> {
4066 SqlDataFrame::execute_stream(self).await
4067 }
4068
4069 fn schema(&self) -> SchemaRef {
4072 SchemaRef::from(self.dataframe.schema().clone())
4073 }
4074
4075 async fn select(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4076 let df = self.dataframe.clone().select_columns(columns)?;
4077 Ok(Box::new(self.with_new_dataframe(df, "select")))
4078 }
4079
4080 async fn select_exprs(
4081 &self,
4082 expressions: &[&krishiv_plan::expression::Expr],
4083 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4084 let expressions = expressions
4085 .iter()
4086 .map(|expression| lower_public_expression(&self.dataframe, expression))
4087 .collect::<Result<Vec<_>, _>>()?;
4088 let df = self.dataframe.clone().select(expressions)?;
4089 Ok(Box::new(self.with_new_dataframe(df, "select_exprs")))
4090 }
4091
4092 async fn aggregate(
4093 &self,
4094 group_exprs: &[&krishiv_plan::expression::Expr],
4095 aggregate_exprs: &[&krishiv_plan::expression::Expr],
4096 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4097 if aggregate_exprs.is_empty() {
4098 return Err(SqlError::Unsupported {
4099 feature: "aggregate requires at least one aggregate expression".into(),
4100 });
4101 }
4102 let group_exprs = group_exprs
4103 .iter()
4104 .map(|expression| lower_public_expression(&self.dataframe, expression))
4105 .collect::<Result<Vec<_>, _>>()?;
4106 let aggregate_exprs = aggregate_exprs
4107 .iter()
4108 .map(|expression| lower_public_expression(&self.dataframe, expression))
4109 .collect::<Result<Vec<_>, _>>()?;
4110 let df = self
4111 .dataframe
4112 .clone()
4113 .aggregate(group_exprs, aggregate_exprs)?;
4114 Ok(Box::new(self.with_new_dataframe(df, "aggregate")))
4115 }
4116
4117 async fn aggregate_grouping(
4118 &self,
4119 grouping: GroupingMode<'_>,
4120 aggregate_exprs: &[&krishiv_plan::expression::Expr],
4121 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4122 if aggregate_exprs.is_empty() {
4123 return Err(SqlError::Unsupported {
4124 feature: "grouping aggregation requires at least one aggregate expression".into(),
4125 });
4126 }
4127 let lower = |expression: &&krishiv_plan::expression::Expr| {
4128 lower_public_expression(&self.dataframe, expression)
4129 };
4130 let group = match grouping {
4131 GroupingMode::Sets(sets) => datafusion::logical_expr::grouping_set(
4132 sets.into_iter()
4133 .map(|set| set.iter().map(lower).collect::<Result<Vec<_>, _>>())
4134 .collect::<Result<Vec<_>, _>>()?,
4135 ),
4136 GroupingMode::Cube(expressions) => datafusion::logical_expr::cube(
4137 expressions
4138 .iter()
4139 .map(lower)
4140 .collect::<Result<Vec<_>, _>>()?,
4141 ),
4142 GroupingMode::Rollup(expressions) => datafusion::logical_expr::rollup(
4143 expressions
4144 .iter()
4145 .map(lower)
4146 .collect::<Result<Vec<_>, _>>()?,
4147 ),
4148 };
4149 let aggregates = aggregate_exprs
4150 .iter()
4151 .map(lower)
4152 .collect::<Result<Vec<_>, _>>()?;
4153 let df = self.dataframe.clone().aggregate(vec![group], aggregates)?;
4154 Ok(Box::new(self.with_new_dataframe(df, "aggregate_grouping")))
4155 }
4156
4157 async fn pivot(
4158 &self,
4159 group_exprs: &[&krishiv_plan::expression::Expr],
4160 pivot_column: &krishiv_plan::expression::Expr,
4161 aggregate_expr: &krishiv_plan::expression::Expr,
4162 values: &[(krishiv_plan::expression::ScalarValue, String)],
4163 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4164 use krishiv_plan::expression::Expr as PublicExpr;
4165 let (function, input, distinct) = match aggregate_expr {
4166 PublicExpr::Aggregate {
4167 function,
4168 expression: Some(input),
4169 distinct,
4170 } => (*function, input.as_ref(), *distinct),
4171 _ => {
4172 return Err(SqlError::Unsupported {
4173 feature: "pivot requires an aggregate expression with one input".into(),
4174 });
4175 }
4176 };
4177 if values.is_empty() {
4178 return Err(SqlError::Unsupported {
4179 feature: "pivot requires at least one value".into(),
4180 });
4181 }
4182 let group_exprs = group_exprs
4183 .iter()
4184 .map(|expression| lower_public_expression(&self.dataframe, expression))
4185 .collect::<Result<Vec<_>, _>>()?;
4186 let aggregates = values
4187 .iter()
4188 .map(|(value, alias)| {
4189 let conditional = PublicExpr::raw(format!(
4190 "CASE WHEN {} = {} THEN {} END",
4191 pivot_column.to_sql(),
4192 value.to_sql_literal(),
4193 input.to_sql()
4194 ));
4195 let aggregate = PublicExpr::Aggregate {
4196 function,
4197 expression: Some(Box::new(conditional)),
4198 distinct,
4199 }
4200 .alias(alias);
4201 lower_public_expression(&self.dataframe, &aggregate)
4202 })
4203 .collect::<Result<Vec<_>, _>>()?;
4204 let dataframe = self.dataframe.clone().aggregate(group_exprs, aggregates)?;
4205 Ok(Box::new(self.with_new_dataframe(dataframe, "pivot")))
4206 }
4207
4208 async fn unpivot(
4209 &self,
4210 columns: &[&str],
4211 name_column: &str,
4212 value_column: &str,
4213 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4214 if columns.is_empty() {
4215 return Err(SqlError::Unsupported {
4216 feature: "unpivot requires at least one column".into(),
4217 });
4218 }
4219 let retained = self
4220 .dataframe
4221 .schema()
4222 .fields()
4223 .iter()
4224 .map(|field| field.name().as_str())
4225 .filter(|name| !columns.contains(name))
4226 .collect::<Vec<_>>();
4227 let mut branches = Vec::with_capacity(columns.len());
4228 for column in columns {
4229 let mut expressions = retained
4230 .iter()
4231 .map(|name| datafusion::logical_expr::col(*name))
4232 .collect::<Vec<_>>();
4233 expressions
4234 .push(datafusion::logical_expr::lit((*column).to_owned()).alias(name_column));
4235 expressions.push(datafusion::logical_expr::col(*column).alias(value_column));
4236 branches.push(self.dataframe.clone().select(expressions)?);
4237 }
4238 let mut branches = branches.into_iter();
4239 let Some(mut dataframe) = branches.next() else {
4240 return Err(SqlError::Unsupported {
4241 feature: "unpivot requires at least one branch".into(),
4242 });
4243 };
4244 for branch in branches {
4245 dataframe = dataframe.union(branch)?;
4246 }
4247 Ok(Box::new(self.with_new_dataframe(dataframe, "unpivot")))
4248 }
4249
4250 async fn filter(&self, predicate: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4251 let expr = self.dataframe.parse_sql_expr(predicate)?;
4252 let df = self.dataframe.clone().filter(expr)?;
4253 Ok(Box::new(self.with_new_dataframe(df, "filter")))
4254 }
4255
4256 async fn filter_expr(
4257 &self,
4258 predicate: &krishiv_plan::expression::Expr,
4259 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4260 let expr = lower_public_expression(&self.dataframe, predicate)?;
4261 let df = self.dataframe.clone().filter(expr)?;
4262 Ok(Box::new(self.with_new_dataframe(df, "filter_expr")))
4263 }
4264
4265 async fn limit(&self, n: usize) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4266 let df = self.dataframe.clone().limit(0, Some(n))?;
4267 Ok(Box::new(self.with_new_dataframe(df, "limit")))
4268 }
4269
4270 async fn distinct(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4271 let df = self.dataframe.clone().distinct()?;
4272 Ok(Box::new(self.with_new_dataframe(df, "distinct")))
4273 }
4274
4275 async fn drop_nulls(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4276 let columns = if columns.is_empty() {
4277 self.dataframe
4278 .schema()
4279 .fields()
4280 .iter()
4281 .map(|field| field.name().as_str())
4282 .collect::<Vec<_>>()
4283 } else {
4284 columns.to_vec()
4285 };
4286 let mut predicate: Option<datafusion::logical_expr::Expr> = None;
4287 for column in columns {
4288 let next = datafusion::logical_expr::col(column).is_not_null();
4289 predicate = Some(match predicate {
4290 Some(current) => current.and(next),
4291 None => next,
4292 });
4293 }
4294 let df = match predicate {
4295 Some(predicate) => self.dataframe.clone().filter(predicate)?,
4296 None => self.dataframe.clone(),
4297 };
4298 Ok(Box::new(self.with_new_dataframe(df, "drop_nulls")))
4299 }
4300
4301 async fn sample(&self, fraction: f64) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4302 if !(0.0..=1.0).contains(&fraction) {
4303 return Err(SqlError::Unsupported {
4304 feature: "sample fraction must be between 0 and 1".into(),
4305 });
4306 }
4307 let predicate = self
4308 .dataframe
4309 .parse_sql_expr(&format!("random() < {fraction}"))?;
4310 let df = self.dataframe.clone().filter(predicate)?;
4311 Ok(Box::new(self.with_new_dataframe(df, "sample")))
4312 }
4313
4314 async fn sort(
4315 &self,
4316 columns: &[&str],
4317 descending: &[bool],
4318 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4319 use datafusion::logical_expr::SortExpr;
4320 let exprs: Vec<SortExpr> = columns
4321 .iter()
4322 .zip(descending.iter())
4323 .map(|(col_name, desc)| datafusion::logical_expr::col(*col_name).sort(!desc, *desc))
4324 .collect();
4325 let df = self.dataframe.clone().sort(exprs)?;
4326 Ok(Box::new(self.with_new_dataframe(df, "sort")))
4327 }
4328
4329 async fn alias(&self, alias: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4330 let df = self.dataframe.clone().alias(alias)?;
4331 Ok(Box::new(self.with_new_dataframe(df, "alias")))
4332 }
4333
4334 async fn drop_columns(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4335 let df = self.dataframe.clone().drop_columns(columns)?;
4336 Ok(Box::new(self.with_new_dataframe(df, "drop")))
4337 }
4338
4339 async fn rename_column(&self, old: &str, new: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4340 let df = self.dataframe.clone().with_column_renamed(old, new)?;
4341 Ok(Box::new(self.with_new_dataframe(df, "rename")))
4342 }
4343
4344 async fn with_column(&self, name: &str, expr: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4345 let parsed = self.dataframe.parse_sql_expr(expr)?;
4346 let df = self.dataframe.clone().with_column(name, parsed)?;
4347 Ok(Box::new(self.with_new_dataframe(df, "with_column")))
4348 }
4349
4350 fn as_any(&self) -> &dyn std::any::Any {
4351 self
4352 }
4353
4354 async fn describe(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4355 let df = self.dataframe.clone().describe().await?;
4356 Ok(Box::new(self.with_new_dataframe(df, "describe")))
4357 }
4358
4359 async fn fill_null(
4360 &self,
4361 column: &str,
4362 value: &str,
4363 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4364 let expr = format!("COALESCE({column}, {value})");
4365 let parsed = self.dataframe.parse_sql_expr(&expr)?;
4366 let df = self.dataframe.clone().with_column(column, parsed)?;
4367 Ok(Box::new(self.with_new_dataframe(df, "fill_null")))
4368 }
4369
4370 async fn join(
4371 &self,
4372 right: &dyn KrishivDataFrameOps,
4373 how: &str,
4374 left_on: &[&str],
4375 right_on: &[&str],
4376 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4377 let right_sql = right
4378 .as_any()
4379 .downcast_ref::<SqlDataFrame>()
4380 .ok_or_else(|| SqlError::DataFusion {
4381 message: "right DataFrame must be SqlDataFrame for join".into(),
4382 })?;
4383 use datafusion::common::JoinType;
4384 let join_type = match how.to_lowercase().as_str() {
4385 "inner" => JoinType::Inner,
4386 "left" => JoinType::Left,
4387 "right" => JoinType::Right,
4388 "full" | "outer" => JoinType::Full,
4389 "leftsemi" | "left_semi" => JoinType::LeftSemi,
4390 "rightsemi" | "right_semi" => JoinType::RightSemi,
4391 "leftanti" | "left_anti" => JoinType::LeftAnti,
4392 "rightanti" | "right_anti" => JoinType::RightAnti,
4393 _ => {
4394 return Err(SqlError::DataFusion {
4395 message: format!("unsupported join type: {how}"),
4396 });
4397 }
4398 };
4399 let df = self.dataframe.clone().join(
4400 right_sql.dataframe.clone(),
4401 join_type,
4402 left_on,
4403 right_on,
4404 None,
4405 )?;
4406 Ok(Box::new(self.with_new_dataframe(df, "join")))
4407 }
4408
4409 async fn union(
4410 &self,
4411 right: &dyn KrishivDataFrameOps,
4412 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4413 let right_sql = right
4414 .as_any()
4415 .downcast_ref::<SqlDataFrame>()
4416 .ok_or_else(|| SqlError::DataFusion {
4417 message: "right DataFrame must be SqlDataFrame for union".into(),
4418 })?;
4419 let df = self.dataframe.clone().union(right_sql.dataframe.clone())?;
4420 Ok(Box::new(self.with_new_dataframe(df, "union")))
4421 }
4422
4423 async fn union_distinct(
4424 &self,
4425 right: &dyn KrishivDataFrameOps,
4426 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4427 let right = sql_dataframe(right, "union_distinct")?;
4428 let df = self
4429 .dataframe
4430 .clone()
4431 .union_distinct(right.dataframe.clone())?;
4432 Ok(Box::new(self.with_new_dataframe(df, "union_distinct")))
4433 }
4434
4435 async fn intersect(
4436 &self,
4437 right: &dyn KrishivDataFrameOps,
4438 distinct: bool,
4439 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4440 let right = sql_dataframe(right, "intersect")?;
4441 let df = if distinct {
4442 self.dataframe
4443 .clone()
4444 .intersect_distinct(right.dataframe.clone())?
4445 } else {
4446 self.dataframe.clone().intersect(right.dataframe.clone())?
4447 };
4448 Ok(Box::new(self.with_new_dataframe(df, "intersect")))
4449 }
4450
4451 async fn except(
4452 &self,
4453 right: &dyn KrishivDataFrameOps,
4454 distinct: bool,
4455 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4456 let right = sql_dataframe(right, "except")?;
4457 let df = if distinct {
4458 self.dataframe
4459 .clone()
4460 .except_distinct(right.dataframe.clone())?
4461 } else {
4462 self.dataframe.clone().except(right.dataframe.clone())?
4463 };
4464 Ok(Box::new(self.with_new_dataframe(df, "except")))
4465 }
4466
4467 async fn register_batches(&self, name: &str, batches: Vec<RecordBatch>) -> SqlResult<()> {
4468 let schema = batches
4469 .first()
4470 .map(|b| b.schema())
4471 .unwrap_or_else(|| Arc::new(arrow::datatypes::Schema::empty()));
4472 let mem_table =
4473 datafusion::datasource::MemTable::try_new(schema, vec![batches]).map_err(|e| {
4474 SqlError::DataFusion {
4475 message: e.to_string(),
4476 }
4477 })?;
4478 self.context
4479 .register_table(name, Arc::new(mem_table))
4480 .map_err(SqlError::from)?;
4481 Ok(())
4482 }
4483
4484 async fn deregister_table(&self, name: &str) -> SqlResult<()> {
4485 let _ = self
4486 .context
4487 .deregister_table(name)
4488 .map_err(SqlError::from)?;
4489 Ok(())
4490 }
4491
4492 async fn create_view(&self, name: &str, replace: bool) -> SqlResult<()> {
4493 let query = self
4494 .query_text
4495 .as_deref()
4496 .ok_or_else(|| SqlError::DataFusion {
4497 message: "create_view requires an SQL query string on the DataFrame".into(),
4498 })?;
4499 let or_replace = if replace { "OR REPLACE " } else { "" };
4500 let safe_name = quote_identifier(name);
4501 let view_sql = format!("CREATE {or_replace}VIEW {safe_name} AS {query}");
4502 self.context.sql(&view_sql).await?;
4503 Ok(())
4504 }
4505}
4506
4507use krishiv_common::sql_util::quote_identifier;
4508
4509#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
4516fn call_args_from_str(s: &str) -> Vec<String> {
4517 let mut args: Vec<String> = Vec::new();
4518 let mut cur = String::new();
4519 let mut in_str = false;
4520 let mut after_str = false;
4521 for ch in s.chars() {
4522 if after_str {
4523 if ch == ',' {
4524 after_str = false;
4525 }
4526 continue;
4527 }
4528 if in_str {
4529 if ch == '\'' {
4530 in_str = false;
4531 after_str = true;
4532 args.push(std::mem::take(&mut cur));
4533 } else {
4534 cur.push(ch);
4535 }
4536 } else if ch == '\'' {
4537 in_str = true;
4538 } else if ch == ',' {
4539 let t = cur.trim().to_string();
4540 if !t.is_empty() {
4541 args.push(t);
4542 }
4543 cur.clear();
4544 } else {
4545 cur.push(ch);
4546 }
4547 }
4548 let t = cur.trim().to_string();
4549 if !t.is_empty() {
4550 args.push(t);
4551 }
4552 args
4553}
4554
4555#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
4562fn iceberg_table_ident(table_ref: &str) -> SqlResult<iceberg::TableIdent> {
4563 let parts: Vec<&str> = table_ref.splitn(3, '.').collect();
4564 match parts.len() {
4565 2 => {
4566 let ns = iceberg::NamespaceIdent::from_vec(vec![
4567 parts.first().copied().unwrap_or("").to_string(),
4568 ])
4569 .map_err(|e| SqlError::DataFusion {
4570 message: e.to_string(),
4571 })?;
4572 Ok(iceberg::TableIdent::new(
4573 ns,
4574 parts.get(1).copied().unwrap_or("").to_string(),
4575 ))
4576 }
4577 3 => {
4578 let ns = iceberg::NamespaceIdent::from_vec(vec![
4579 parts.get(1).copied().unwrap_or("").to_string(),
4580 ])
4581 .map_err(|e| SqlError::DataFusion {
4582 message: e.to_string(),
4583 })?;
4584 Ok(iceberg::TableIdent::new(
4585 ns,
4586 parts.get(2).copied().unwrap_or("").to_string(),
4587 ))
4588 }
4589 _ => Err(SqlError::DataFusion {
4590 message: format!(
4591 "invalid table reference '{table_ref}': expected 'ns.table' or 'cat.ns.table'"
4592 ),
4593 }),
4594 }
4595}
4596
4597#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
4602fn parse_call_duration(s: &str) -> SqlResult<chrono::Duration> {
4603 let s = s.trim();
4604 let mut it = s.splitn(2, ' ');
4605 let n: i64 = it
4606 .next()
4607 .and_then(|v| v.parse().ok())
4608 .ok_or_else(|| SqlError::DataFusion {
4609 message: format!("invalid duration value in '{s}'"),
4610 })?;
4611 let unit = it.next().unwrap_or("").trim().to_ascii_lowercase();
4612 match unit.trim_end_matches('s') {
4613 "day" => Ok(chrono::Duration::days(n)),
4614 "hour" => Ok(chrono::Duration::hours(n)),
4615 "week" => Ok(chrono::Duration::weeks(n)),
4616 "minute" | "min" => Ok(chrono::Duration::minutes(n)),
4617 _ => Err(SqlError::DataFusion {
4618 message: format!("unknown duration unit '{unit}' in '{s}'"),
4619 }),
4620 }
4621}
4622
4623#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
4631fn parse_dml_delete(stmt: &str) -> Option<(String, String)> {
4632 use datafusion::sql::sqlparser::ast::{FromTable, Statement, TableFactor};
4633 use datafusion::sql::sqlparser::dialect::GenericDialect;
4634 use datafusion::sql::sqlparser::parser::Parser;
4635
4636 let mut stmts = Parser::parse_sql(&GenericDialect {}, stmt).ok()?;
4637 if stmts.len() != 1 {
4638 return None;
4639 }
4640 let Statement::Delete(delete) = stmts.remove(0) else {
4641 return None;
4642 };
4643 let tables = match delete.from {
4646 FromTable::WithFromKeyword(tables) | FromTable::WithoutKeyword(tables) => tables,
4647 };
4648 let first_from = tables.into_iter().next()?;
4649 let table_name = match first_from.relation {
4650 TableFactor::Table { name, .. } => name.to_string(),
4651 _ => return None,
4652 };
4653 let predicate = delete
4654 .selection
4655 .map(|e| e.to_string())
4656 .unwrap_or_else(|| "TRUE".to_string());
4657 Some((table_name, predicate))
4658}
4659
4660#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
4662struct ParsedCtas {
4663 table_ref: String,
4665 or_replace: bool,
4666 inner_query: String,
4668 partition_by: Vec<String>,
4671}
4672
4673#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
4683fn extract_partitioned_by(stmt: &str) -> Option<(String, Vec<String>)> {
4684 let bytes = stmt.as_bytes();
4685 let upper = stmt.to_ascii_uppercase();
4686 let upper_bytes = upper.as_bytes();
4687 const NEEDLE: &[u8] = b"PARTITIONED";
4688
4689 fn is_ident_byte(b: u8) -> bool {
4690 b.is_ascii_alphanumeric() || b == b'_'
4691 }
4692 fn skip_quoted(bytes: &[u8], mut i: usize, quote: u8) -> usize {
4695 i += 1;
4696 while let Some(&b) = bytes.get(i) {
4697 if b == quote {
4698 if bytes.get(i + 1) == Some("e) {
4699 i += 2;
4700 continue;
4701 }
4702 return i + 1;
4703 }
4704 i += 1;
4705 }
4706 i
4707 }
4708
4709 let mut i = 0;
4710 while let Some(&b) = bytes.get(i) {
4711 match b {
4712 b'\'' | b'"' => i = skip_quoted(bytes, i, b),
4713 _ => {
4714 let at_needle = upper_bytes
4715 .get(i..)
4716 .is_some_and(|rest| rest.starts_with(NEEDLE))
4717 && (i == 0
4718 || !i
4719 .checked_sub(1)
4720 .and_then(|p| upper_bytes.get(p))
4721 .copied()
4722 .is_some_and(is_ident_byte));
4723 if at_needle {
4724 let mut j = i + NEEDLE.len();
4725 while bytes.get(j).is_some_and(u8::is_ascii_whitespace) {
4726 j += 1;
4727 }
4728 if j > i + NEEDLE.len()
4731 && upper_bytes
4732 .get(j..)
4733 .is_some_and(|rest| rest.starts_with(b"BY"))
4734 && !upper_bytes.get(j + 2).copied().is_some_and(is_ident_byte)
4735 {
4736 let mut k = j + 2;
4737 while bytes.get(k).is_some_and(u8::is_ascii_whitespace) {
4738 k += 1;
4739 }
4740 if bytes.get(k) == Some(&b'(') {
4741 let mut depth = 0i32;
4743 let mut c = k;
4744 let close = loop {
4745 match bytes.get(c) {
4746 None => return None,
4748 Some(b'(') => depth += 1,
4749 Some(b')') => {
4750 depth -= 1;
4751 if depth == 0 {
4752 break c;
4753 }
4754 }
4755 Some(&(q @ b'\'' | q @ b'"')) => {
4756 c = skip_quoted(bytes, c, q);
4757 continue;
4758 }
4759 Some(_) => {}
4760 }
4761 c += 1;
4762 };
4763 let body = stmt.get(k + 1..close)?;
4764 let head = stmt.get(..i)?.trim_end();
4765 let tail = stmt.get(close + 1..)?.trim_start();
4766 let items = split_top_level_commas(body);
4767 let mut remainder = String::with_capacity(stmt.len());
4768 remainder.push_str(head);
4769 remainder.push(' ');
4770 remainder.push_str(tail);
4771 return Some((remainder, items));
4772 }
4773 }
4774 }
4775 i += 1;
4776 }
4777 }
4778 }
4779 None
4780}
4781
4782#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
4785fn split_top_level_commas(s: &str) -> Vec<String> {
4786 let bytes = s.as_bytes();
4787 let mut items = Vec::new();
4788 let mut depth = 0i32;
4789 let mut start = 0usize;
4790 let mut i = 0;
4791 while let Some(&b) = bytes.get(i) {
4792 match b {
4793 b'(' => depth += 1,
4794 b')' => depth -= 1,
4795 b'\'' | b'"' => {
4796 i += 1;
4797 while bytes.get(i).is_some_and(|&c| c != b) {
4798 i += 1;
4799 }
4800 }
4801 b',' if depth == 0 => {
4802 if let Some(item) = s.get(start..i).map(str::trim)
4803 && !item.is_empty()
4804 {
4805 items.push(item.to_string());
4806 }
4807 start = i + 1;
4808 }
4809 _ => {}
4810 }
4811 i += 1;
4812 }
4813 if let Some(last) = s.get(start..).map(str::trim)
4814 && !last.is_empty()
4815 {
4816 items.push(last.to_string());
4817 }
4818 items
4819}
4820
4821#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
4829fn parse_ctas(stmt: &str) -> Option<ParsedCtas> {
4830 use datafusion::sql::sqlparser::ast::Statement;
4831 use datafusion::sql::sqlparser::dialect::GenericDialect;
4832 use datafusion::sql::sqlparser::parser::Parser;
4833
4834 let (stripped, partition_by) = match extract_partitioned_by(stmt) {
4835 Some((remainder, items)) => (remainder, items),
4836 None => (stmt.to_string(), Vec::new()),
4837 };
4838 let mut stmts = Parser::parse_sql(&GenericDialect {}, &stripped).ok()?;
4839 if stmts.len() != 1 {
4840 return None;
4841 }
4842 let Statement::CreateTable(create) = stmts.remove(0) else {
4843 return None;
4844 };
4845 if create.external || create.temporary {
4846 return None;
4847 }
4848 let inner_query = create.query?.to_string();
4849 Some(ParsedCtas {
4850 table_ref: create.name.to_string(),
4851 or_replace: create.or_replace,
4852 inner_query,
4853 partition_by,
4854 })
4855}
4856
4857#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
4859struct ParsedUpdate {
4860 table_ref: String,
4861 assignments: Vec<(String, String)>,
4863 predicate: Option<String>,
4864}
4865
4866#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
4872fn parse_dml_update(stmt: &str) -> Option<ParsedUpdate> {
4873 use datafusion::sql::sqlparser::ast::{Statement, TableFactor};
4874 use datafusion::sql::sqlparser::dialect::GenericDialect;
4875 use datafusion::sql::sqlparser::parser::Parser;
4876
4877 let mut stmts = Parser::parse_sql(&GenericDialect {}, stmt).ok()?;
4878 if stmts.len() != 1 {
4879 return None;
4880 }
4881 let Statement::Update(update) = stmts.remove(0) else {
4883 return None;
4884 };
4885 let table_name = match update.table.relation {
4886 TableFactor::Table { name, .. } => name.to_string(),
4887 _ => return None,
4888 };
4889 let parsed_assignments: Vec<(String, String)> = update
4891 .assignments
4892 .into_iter()
4893 .map(|a| {
4894 let col = a.target.to_string();
4896 let val = a.value.to_string();
4897 (col, val)
4898 })
4899 .collect();
4900 if parsed_assignments.is_empty() {
4901 return None;
4902 }
4903 Some(ParsedUpdate {
4904 table_ref: table_name,
4905 assignments: parsed_assignments,
4906 predicate: update.selection.map(|e| e.to_string()),
4907 })
4908}
4909
4910pub fn plan_sql(query: impl Into<String>) -> SqlResult<SqlPlan> {
4912 let query = query.into();
4913 if query.trim().is_empty() {
4914 return Err(SqlError::EmptyQuery);
4915 }
4916
4917 if let Some(stmt) = cep_sql::parse_match_recognize(&query)? {
4918 let logical_plan = cep_sql::plan_match_recognize(stmt, &query);
4919 let optimized = Optimizer::default().optimize(logical_plan)?;
4920 return Ok(SqlPlan {
4921 query,
4922 logical_plan: optimized.plan,
4923 });
4924 }
4925
4926 let logical_plan =
4927 LogicalPlan::new("sql-query", ExecutionKind::Batch).with_node(PlanNode::new(
4928 "sql",
4929 format!("sql: {}", query.trim()),
4930 ExecutionKind::Batch,
4931 ));
4932
4933 let optimized = Optimizer::default().optimize(logical_plan)?;
4934 Ok(SqlPlan {
4935 query,
4936 logical_plan: optimized.plan,
4937 })
4938}
4939
4940pub fn explain_sql(query: impl Into<String>) -> SqlResult<String> {
4942 let plan = plan_sql(query)?;
4943 Ok(plan.logical_plan().describe())
4944}
4945
4946pub fn explain_sql_optimized(query: impl Into<String>, optimizer: &Optimizer) -> SqlResult<String> {
4951 let plan = plan_sql(query)?;
4952 let result = optimizer.optimize(plan.logical_plan().clone())?;
4953 let mut output = result.plan.describe();
4954 let optimizer_line = result.describe();
4955 output.push('\n');
4956 output.push_str(&optimizer_line);
4957 Ok(output)
4958}
4959
4960pub fn explain_sql_with_cost(
4962 query: impl Into<String>,
4963 cost_model: &dyn CostModel,
4964) -> SqlResult<String> {
4965 let plan = plan_sql(query)?;
4966 let cost = cost_model.estimate(plan.logical_plan());
4967 let mut output = plan.logical_plan().describe();
4968 output.push_str(&format!(
4969 "\ncost: cpu_nanos={}, memory_bytes={}, network_bytes={}",
4970 cost.cpu_nanos, cost.memory_bytes, cost.network_bytes
4971 ));
4972 Ok(output)
4973}
4974
4975pub fn referenced_table_names(query: impl AsRef<str>) -> SqlResult<Vec<String>> {
4981 let query = query.as_ref();
4982 if query.trim().is_empty() {
4983 return Err(SqlError::EmptyQuery);
4984 }
4985
4986 let statements =
4987 Parser::parse_sql(&GenericDialect {}, query).map_err(|e| SqlError::DataFusion {
4988 message: format!("SQL parse error: {e}"),
4989 })?;
4990 let mut names = BTreeSet::new();
4991 let _ = visit_relations(&statements, |relation| {
4992 names.insert(relation.to_string());
4993 ControlFlow::<()>::Continue(())
4994 });
4995 Ok(names.into_iter().collect())
4996}
4997
4998pub fn pretty_batches(batches: &[RecordBatch]) -> SqlResult<String> {
5000 Ok(pretty_format_batches(batches)
5001 .map_err(|error| SqlError::DataFusion {
5002 message: error.to_string(),
5003 })?
5004 .to_string())
5005}
5006
5007#[cfg(test)]
5008mod sql_tests;