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
28fn python_udf_arrow_type(name: &str) -> arrow::datatypes::DataType {
40 use arrow::datatypes::DataType;
41 match name.trim().to_ascii_lowercase().as_str() {
42 "double" | "float64" | "float" => DataType::Float64,
43 "float32" | "real" => DataType::Float32,
44 "int" | "int64" | "bigint" | "long" => DataType::Int64,
45 "int32" | "integer" => DataType::Int32,
46 "bool" | "boolean" => DataType::Boolean,
47 "utf8" | "string" | "varchar" | "text" => DataType::Utf8,
48 _ => DataType::Utf8,
49 }
50}
51
52pub(crate) fn build_s3_object_store(
53 bucket: &str,
54) -> object_store::Result<std::sync::Arc<dyn object_store::ObjectStore>> {
55 let mut builder = AmazonS3Builder::from_env().with_bucket_name(bucket);
56 let mut has_endpoint = false;
57 if let Ok(endpoint) = std::env::var("AWS_ENDPOINT_URL")
58 && !endpoint.is_empty()
59 {
60 builder = builder.with_endpoint(endpoint).with_allow_http(true);
62 has_endpoint = true;
63 }
64 let has_key = std::env::var("AWS_ACCESS_KEY_ID")
65 .map(|k| !k.is_empty())
66 .unwrap_or(false);
67 if let Ok(key) = std::env::var("AWS_ACCESS_KEY_ID")
68 && !key.is_empty()
69 {
70 builder = builder.with_access_key_id(key);
71 }
72 if let Ok(secret) = std::env::var("AWS_SECRET_ACCESS_KEY")
73 && !secret.is_empty()
74 {
75 builder = builder.with_secret_access_key(secret);
76 }
77 if has_endpoint && !has_key {
84 builder = builder.with_skip_signature(true);
85 }
86 let region = std::env::var("AWS_REGION")
87 .or_else(|_| std::env::var("AWS_DEFAULT_REGION"))
88 .unwrap_or_else(|_| "us-east-1".to_string());
89 builder = builder.with_region(region);
90 Ok(std::sync::Arc::new(builder.build()?))
91}
92
93pub mod analyze;
94pub mod catalog;
95pub mod cep_sql;
96
97pub mod connector_table;
98pub mod coop_amplifiers;
99pub mod create_function_ddl;
100pub mod distributed_plan;
101pub mod grace_hash_join;
102pub mod grammar;
103pub mod incremental_view;
104pub mod introspection_sql;
105
106pub mod kafka_table;
107pub mod lakehouse;
108pub mod late_materialize;
109pub mod live_table;
110pub(crate) mod join_estimates;
113pub mod object_store_registry;
114pub mod pipeline_ddl;
115pub mod pivot_sql;
116pub mod python_udf;
117pub mod scalar_udf;
118pub mod semi_join_reduction;
119pub mod spark_sql_ext;
121pub mod runtime_filter_exec;
122pub mod spillable_join;
123pub mod sqlstate;
124pub mod subquery;
125pub mod unnest_sql;
126pub mod unspillable_headroom;
127
128pub mod coverage;
129mod higher_order_functions;
130mod json_functions;
131mod spark_functions;
132pub mod statement_completion;
133pub mod streaming;
134pub mod streaming_table_ddl;
135pub mod streaming_tvf;
136pub mod streaming_window_plan;
137mod udf;
138mod window_functions;
139
140pub use cep_sql::{
141 MatchRecognizeStatement, execute_streaming_match_recognize, parse_match_recognize,
142};
143pub use lakehouse::{AsOfTableRef, MergeResult, MergeTargetUnsupportedError, preprocess_as_of_sql};
144
145pub use grammar::{
146 FeatureEntry, FeatureStatus, feature_matrix, features_by_status, features_for_category,
147};
148pub use sqlstate::{SqlStateError, sqlstate_for};
149pub use streaming::{ContinuousInputError, ContinuousTableInput};
150
151pub type SqlResult<T> = Result<T, SqlError>;
153
154pub type SqlStream =
160 std::pin::Pin<Box<dyn futures::stream::Stream<Item = Result<RecordBatch, SqlError>> + Send>>;
161
162static EPHEMERAL_TABLE_COUNTER: AtomicU64 = AtomicU64::new(0);
165
166fn next_ephemeral_name(prefix: &str) -> String {
167 let id = EPHEMERAL_TABLE_COUNTER.fetch_add(1, Ordering::Relaxed);
168 format!("__{prefix}_{id}")
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176enum WindowFnRegistration {
177 Register,
179 Skip,
183}
184
185struct PlanCache {
191 map: HashMap<String, (datafusion::logical_expr::LogicalPlan, std::time::Instant)>,
192 order: VecDeque<String>,
193 max: usize,
194}
195
196const PLAN_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(30);
208
209impl PlanCache {
210 fn new(max: usize) -> Self {
211 Self {
212 map: HashMap::new(),
213 order: VecDeque::new(),
214 max,
215 }
216 }
217
218 fn get(&self, key: &str) -> Option<&datafusion::logical_expr::LogicalPlan> {
219 self.map
220 .get(key)
221 .filter(|(_, at)| at.elapsed() < PLAN_CACHE_TTL)
222 .map(|(plan, _)| plan)
223 }
224
225 fn insert(&mut self, key: String, plan: datafusion::logical_expr::LogicalPlan) {
226 if self.map.contains_key(&key) {
227 self.order.retain(|k| k != &key);
230 } else if self.map.len() >= self.max
231 && let Some(oldest) = self.order.pop_front()
232 {
233 self.map.remove(&oldest);
234 }
235 self.order.push_back(key.clone());
236 self.map.insert(key, (plan, std::time::Instant::now()));
237 }
238
239 fn clear(&mut self) {
240 self.map.clear();
241 self.order.clear();
242 }
243
244 #[cfg(test)]
245 fn is_empty(&self) -> bool {
246 self.map.is_empty()
247 }
248}
249
250#[derive(Debug, Clone, Default)]
252pub struct ParquetReaderOptions {
253 pub batch_size: Option<usize>,
255}
256
257#[derive(Debug, Clone, Default)]
259pub struct CsvReaderOptions {
260 pub delimiter: Option<char>,
262 pub has_header: Option<bool>,
264}
265
266#[derive(Debug, Clone, Default)]
268pub struct ParquetWriterOptions {
269 pub compression: Option<String>,
271 pub max_row_group_size: Option<usize>,
273}
274
275#[derive(Debug, Clone, Default)]
277pub struct CsvWriterOptions {
278 pub delimiter: Option<char>,
280 pub has_header: Option<bool>,
282}
283
284#[non_exhaustive]
286#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
287pub enum SqlError {
288 #[error("SQL query is empty")]
290 EmptyQuery,
291 #[error("table name is empty")]
293 EmptyTableName,
294 #[error("unsupported SQL feature: {feature}")]
296 Unsupported { feature: String },
297 #[error("invalid table function: {message}")]
299 InvalidTableFunction { message: String },
300 #[error("DataFusion error: {message}")]
302 DataFusion { message: String },
303 #[error(transparent)]
305 Optimizer(#[from] krishiv_plan::optimizer::OptimizerError),
306 #[error("access denied: {reason}")]
308 AccessDenied { reason: String },
309 #[error("operation {operation_id} was cancelled")]
311 OperationCancelled { operation_id: u64 },
312 #[error("query timed out after {timeout_ms} ms")]
314 Timeout { timeout_ms: u64 },
315}
316
317impl From<datafusion::error::DataFusionError> for SqlError {
318 fn from(value: datafusion::error::DataFusionError) -> Self {
319 Self::DataFusion {
320 message: value.to_string(),
321 }
322 }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct SqlPlan {
328 query: String,
329 logical_plan: LogicalPlan,
330}
331
332impl SqlPlan {
333 pub fn query(&self) -> &str {
335 &self.query
336 }
337
338 pub fn logical_plan(&self) -> &LogicalPlan {
340 &self.logical_plan
341 }
342}
343
344const PLAN_CACHE_MAX_ENTRIES: usize = 256;
356
357fn resolve_plan_cache_max_entries() -> usize {
358 std::env::var("KRISHIV_PLAN_CACHE_MAX_ENTRIES")
359 .ok()
360 .and_then(|v| v.parse().ok())
361 .filter(|&n| n > 0)
362 .unwrap_or(PLAN_CACHE_MAX_ENTRIES)
363}
364const STREAMING_CEP_MAX_ROWS_DEFAULT: usize = 100_000;
365
366pub fn resolve_streaming_match_recognize_limit(raw: Option<&str>) -> usize {
370 raw.and_then(|s| s.parse::<usize>().ok())
371 .filter(|n| *n > 0)
372 .unwrap_or(STREAMING_CEP_MAX_ROWS_DEFAULT)
373}
374
375pub fn streaming_match_recognize_limit_from_env() -> usize {
378 resolve_streaming_match_recognize_limit(
379 std::env::var("KRISHIV_MATCH_RECOGNIZE_STREAMING_LIMIT")
380 .ok()
381 .as_deref(),
382 )
383}
384
385pub fn resolve_query_memory_limit_bytes(raw: Option<&str>) -> Option<usize> {
389 raw.and_then(|s| s.trim().parse::<usize>().ok())
390 .filter(|n| *n > 0)
391}
392
393pub fn query_memory_limit_from_env() -> Option<usize> {
404 match std::env::var("KRISHIV_QUERY_MEMORY_LIMIT_BYTES").ok() {
405 Some(raw) => resolve_query_memory_limit_bytes(Some(&raw)),
408 None => cgroup_memory_limit_bytes()
409 .map(|limit| (limit / 4) as usize)
410 .filter(|&n| n > 0),
411 }
412}
413
414pub use krishiv_common::cgroup_memory_limit_bytes;
415
416pub use datafusion::execution::memory_pool::MemoryPool;
420
421pub use datafusion::execution::memory_pool::{MemoryConsumer, MemoryReservation};
428
429pub use datafusion::physical_plan::RecordBatchStream;
438
439pub fn process_query_pool() -> Option<&'static Arc<dyn MemoryPool>> {
450 static POOL: std::sync::LazyLock<Option<Arc<dyn MemoryPool>>> =
451 std::sync::LazyLock::new(|| {
452 let capacity = krishiv_common::ExecutorCapacity::detect();
453 let bytes = capacity.query_pool_bytes?;
454 tracing::info!(
455 capacity = %capacity.summary(),
456 "query memory: one shared FairSpillPool for this process"
457 );
458 Some(EngineMemory::shared_pool(
459 usize::try_from(bytes).unwrap_or(usize::MAX),
460 ))
461 });
462 POOL.as_ref()
463}
464
465fn guarded_fair_pool(bytes: usize) -> Arc<dyn MemoryPool> {
480 let inner: Arc<dyn MemoryPool> =
481 Arc::new(datafusion::execution::memory_pool::FairSpillPool::new(bytes));
482 Arc::new(crate::unspillable_headroom::UnspillableHeadroomPool::new(
483 inner,
484 bytes,
485 crate::unspillable_headroom::headroom_bytes(bytes),
486 ))
487}
488
489fn process_query_pool_fair_share_bytes() -> usize {
493 krishiv_common::ExecutorCapacity::detect()
494 .min_task_memory_share_bytes()
495 .map_or(usize::MAX, |bytes| {
496 usize::try_from(bytes).unwrap_or(usize::MAX)
497 })
498}
499
500#[derive(Clone)]
512pub enum EngineMemory {
513 Unbounded,
515 Private(usize),
518 Shared {
524 pool: Arc<dyn datafusion::execution::memory_pool::MemoryPool>,
526 fair_share_bytes: usize,
528 },
529}
530
531impl EngineMemory {
532 #[must_use]
535 pub fn from_limit(bytes: Option<usize>) -> Self {
536 bytes.map_or(Self::Unbounded, Self::Private)
537 }
538
539 #[must_use]
545 pub fn shared_pool(bytes: usize) -> Arc<dyn MemoryPool> {
546 guarded_fair_pool(bytes)
547 }
548
549 #[must_use]
561 pub fn for_this_process() -> Self {
562 match process_query_pool() {
563 Some(pool) => Self::Shared {
564 pool: Arc::clone(pool),
565 fair_share_bytes: process_query_pool_fair_share_bytes(),
566 },
567 None => Self::Unbounded,
568 }
569 }
570
571 #[must_use]
574 pub fn sizing_bytes(&self) -> Option<usize> {
575 match self {
576 Self::Unbounded => None,
577 Self::Private(bytes) => Some(*bytes),
578 Self::Shared {
579 fair_share_bytes, ..
580 } => Some(*fair_share_bytes),
581 }
582 }
583
584 fn pool(&self) -> Option<Arc<dyn datafusion::execution::memory_pool::MemoryPool>> {
586 match self {
587 Self::Unbounded => None,
588 Self::Private(bytes) => Some(guarded_fair_pool(*bytes)),
594 Self::Shared { pool, .. } => Some(Arc::clone(pool)),
595 }
596 }
597}
598
599impl fmt::Debug for EngineMemory {
600 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
601 match self {
602 Self::Unbounded => f.write_str("EngineMemory::Unbounded"),
603 Self::Private(bytes) => write!(f, "EngineMemory::Private({bytes})"),
604 Self::Shared {
605 fair_share_bytes, ..
606 } => write!(f, "EngineMemory::Shared(share={fair_share_bytes})"),
607 }
608 }
609}
610
611static RUNTIME_FILTERS_OVERRIDE: std::sync::atomic::AtomicU8 =
616 std::sync::atomic::AtomicU8::new(u8::MAX);
617
618#[doc(hidden)]
621pub fn set_runtime_filters_for_tests(enabled: bool) {
622 RUNTIME_FILTERS_OVERRIDE.store(u8::from(enabled), std::sync::atomic::Ordering::Relaxed);
623}
624
625pub fn runtime_filters_enabled_from_env() -> bool {
630 match RUNTIME_FILTERS_OVERRIDE.load(std::sync::atomic::Ordering::Relaxed) {
631 0 => return false,
632 1 => return true,
633 _ => {}
634 }
635 !matches!(
636 std::env::var("KRISHIV_RUNTIME_FILTERS")
637 .unwrap_or_default()
638 .trim()
639 .to_ascii_lowercase()
640 .as_str(),
641 "off" | "0" | "false" | "disabled"
642 )
643}
644
645pub fn batch_size_from_env() -> usize {
649 std::env::var("KRISHIV_BATCH_SIZE")
650 .ok()
651 .and_then(|v| v.parse::<usize>().ok())
652 .filter(|n| *n > 0)
653 .unwrap_or(8192)
654}
655
656pub fn default_parallelism_from_env() -> NonZeroUsize {
660 std::env::var("KRISHIV_TARGET_PARALLELISM")
661 .ok()
662 .and_then(|v| v.parse::<usize>().ok())
663 .and_then(NonZeroUsize::new)
664 .unwrap_or_else(|| std::thread::available_parallelism().unwrap_or(NonZeroUsize::MIN))
665}
666
667const DEFAULT_SORT_SPILL_RESERVATION_BYTES: usize = 10 * 1024 * 1024;
673
674const MIN_SORT_SPILL_RESERVATION_BYTES: usize = 64 * 1024;
677
678#[must_use]
720pub fn with_krishiv_optimizer_rules(
721 builder: datafusion::execution::session_state::SessionStateBuilder,
722) -> datafusion::execution::session_state::SessionStateBuilder {
723 with_krishiv_optimizer_rules_with_join_threshold(builder, None)
724}
725
726#[must_use]
729pub fn with_krishiv_optimizer_rules_with_join_threshold(
730 builder: datafusion::execution::session_state::SessionStateBuilder,
731 spill_join_build_bytes: Option<u64>,
732) -> datafusion::execution::session_state::SessionStateBuilder {
733 let spillable_join = match spill_join_build_bytes {
734 Some(bytes) => crate::spillable_join::SpillableJoinSelection::with_threshold(Some(bytes)),
747 None => crate::spillable_join::SpillableJoinSelection::from_capacity(),
748 };
749 builder
750 .with_physical_optimizer_rule(std::sync::Arc::new(
751 crate::coop_amplifiers::CooperativeAmplifiers::new(),
752 ))
753 .with_physical_optimizer_rule(std::sync::Arc::new(spillable_join))
759 .with_optimizer_rule(std::sync::Arc::new(
762 crate::semi_join_reduction::SemiJoinReductionThroughAggregate,
763 ))
764 .with_optimizer_rule(std::sync::Arc::new(
773 crate::semi_join_reduction::SemiJoinPushdownThroughInnerJoin::default(),
774 ))
775 .with_optimizer_rule(std::sync::Arc::new(
787 crate::late_materialize::LateMaterializeTopKAggregate::default(),
788 ))
789}
790
791pub(crate) fn build_single_node_session_config(
792 target_partitions: NonZeroUsize,
793 memory_limit_bytes: Option<usize>,
794) -> datafusion::prelude::SessionConfig {
795 let tp = target_partitions.get();
796 let batch_size = batch_size_from_env();
797 let mut config = datafusion::prelude::SessionConfig::new()
798 .with_target_partitions(tp)
799 .with_batch_size(batch_size)
800 .with_information_schema(true)
801 .set_bool(
802 "datafusion.optimizer.enable_round_robin_repartition",
803 tp > 1,
804 )
805 .set_bool(
810 "datafusion.optimizer.enable_dynamic_filter_pushdown",
811 runtime_filters_enabled_from_env(),
812 )
813 .set_bool(
814 "datafusion.optimizer.enable_join_dynamic_filter_pushdown",
815 runtime_filters_enabled_from_env(),
816 )
817 .set_bool(
818 "datafusion.optimizer.enable_topk_dynamic_filter_pushdown",
819 runtime_filters_enabled_from_env(),
820 )
821 .set_bool(
822 "datafusion.optimizer.enable_aggregate_dynamic_filter_pushdown",
823 runtime_filters_enabled_from_env(),
824 );
825 config.options_mut().sql_parser.dialect = datafusion::common::config::Dialect::DuckDB;
833 if let Some(limit) = memory_limit_bytes {
840 let scaled = (limit / 4).clamp(
841 MIN_SORT_SPILL_RESERVATION_BYTES,
842 DEFAULT_SORT_SPILL_RESERVATION_BYTES,
843 );
844 config = config.with_sort_spill_reservation_bytes(scaled);
845 }
846 config
847}
848
849#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
853type IcebergCatalogRegistry =
854 Arc<std::sync::RwLock<Vec<(Arc<catalog::unified::KrishivCatalog>, String)>>>;
855
856#[derive(Clone)]
857pub struct SqlEngine {
858 context: SessionContext,
859 target_parallelism: NonZeroUsize,
860 krishiv_catalog: Option<Arc<RwLock<InMemoryCatalog>>>,
861 udf_registry: Option<std::sync::Arc<std::sync::RwLock<krishiv_plan::udf::UdfRegistry>>>,
862 streaming_sources: Arc<RwLock<std::collections::HashSet<String>>>,
865 streaming_registration: Arc<Mutex<()>>,
867 has_streaming_sources: Arc<AtomicBool>,
872 udf_limits: Option<krishiv_plan::udf::ResourceLimits>,
875 udf_registry_version: Arc<AtomicU64>,
879 udf_last_synced_version: Arc<AtomicU64>,
882 plan_cache: Arc<Mutex<PlanCache>>,
888 shuffle_partitions: Arc<std::sync::RwLock<Option<u32>>>,
891 table_row_counts: Arc<std::sync::RwLock<HashMap<String, u64>>>,
896 memory_limit_bytes: Option<usize>,
901 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
905 iceberg_catalogs: IcebergCatalogRegistry,
906 live_table_registry: Arc<live_table::LiveTableRegistry>,
908 incremental_view_registry: Arc<incremental_view::IncrementalViewRegistry>,
910 pipeline_registry: Arc<pipeline_ddl::PipelineRegistry>,
912 operation_registry: Arc<OperationRegistry>,
914}
915
916impl fmt::Debug for SqlEngine {
917 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
918 f.debug_struct("SqlEngine")
919 .field("backend", &"datafusion")
920 .finish_non_exhaustive()
921 }
922}
923
924impl Default for SqlEngine {
925 fn default() -> Self {
926 Self::new()
927 }
928}
929
930impl SqlEngine {
931 pub fn new() -> Self {
945 Self::new_with_engine_memory(EngineMemory::for_this_process())
946 }
947
948 pub fn new_with_memory_limit(memory_limit_bytes: Option<usize>) -> Self {
960 Self::new_with_engine_memory(EngineMemory::from_limit(memory_limit_bytes))
961 }
962
963 pub fn new_with_engine_memory(engine_memory: EngineMemory) -> Self {
973 let parallelism = default_parallelism_from_env();
974 match Self::build_local(
975 None,
976 WindowFnRegistration::Register,
977 parallelism,
978 engine_memory.clone(),
979 ) {
980 Ok(engine) => engine,
981 Err(err) => {
982 tracing::warn!(
983 error = %err,
984 "SqlEngine::new: window helper UDF registration failed; \
985 window SQL functions will be unavailable, other queries are unaffected"
986 );
987 Self::build_local(
988 None,
989 WindowFnRegistration::Skip,
990 parallelism,
991 engine_memory.clone(),
992 )
993 .unwrap_or_else(|err| {
994 tracing::error!(
995 error = %err,
996 "memory-limited DataFusion runtime construction failed; \
997 falling back to an unbounded engine"
998 );
999 Self::build_local(
1000 None,
1001 WindowFnRegistration::Skip,
1002 parallelism,
1003 EngineMemory::Unbounded,
1004 )
1005 .unwrap_or_else(|_| Self::build_absolute_minimal(parallelism))
1006 })
1007 }
1008 }
1009 }
1010
1011 pub fn try_new() -> SqlResult<Self> {
1016 Self::build_local(
1017 None,
1018 WindowFnRegistration::Register,
1019 default_parallelism_from_env(),
1020 EngineMemory::for_this_process(),
1021 )
1022 }
1023
1024 pub fn with_in_memory_catalog(catalog: Arc<RwLock<InMemoryCatalog>>) -> SqlResult<Self> {
1026 if krishiv_common::profile_requires_fail_closed_metadata(
1027 krishiv_common::resolve_durability_profile(),
1028 ) {
1029 return Err(SqlError::DataFusion {
1030 message: String::from(
1031 "InMemoryCatalog is dev-only; configure a durable REST or file-backed \
1032 catalog for production deployments",
1033 ),
1034 });
1035 }
1036 Self::build_local(
1037 Some(catalog),
1038 WindowFnRegistration::Register,
1039 default_parallelism_from_env(),
1040 EngineMemory::for_this_process(),
1041 )
1042 }
1043
1044 #[must_use]
1055 pub fn with_target_parallelism(mut self, n: NonZeroUsize) -> Self {
1056 self.target_parallelism = n;
1057 self.apply_target_partitions(n);
1058 self
1059 }
1060
1061 fn apply_target_partitions(&self, n: NonZeroUsize) {
1070 let state_ref = self.context.state_ref();
1071 let mut state = state_ref.write();
1072 let options = state.config_mut().options_mut();
1073 options.execution.target_partitions = n.get();
1074 options.optimizer.enable_round_robin_repartition = n.get() > 1;
1075 }
1076
1077 pub fn target_parallelism(&self) -> NonZeroUsize {
1079 self.target_parallelism
1080 }
1081
1082 pub fn memory_limit_bytes(&self) -> Option<usize> {
1084 self.memory_limit_bytes
1085 }
1086
1087 pub fn session_context(&self) -> &SessionContext {
1093 &self.context
1094 }
1095
1096 pub fn shuffle_partitions(&self) -> Option<u32> {
1098 *self
1099 .shuffle_partitions
1100 .read()
1101 .unwrap_or_else(|e| e.into_inner())
1102 }
1103
1104 pub fn table_row_counts(&self) -> Arc<std::sync::RwLock<HashMap<String, u64>>> {
1110 Arc::clone(&self.table_row_counts)
1111 }
1112
1113 pub fn registered_table_names(&self) -> Vec<String> {
1119 let mut names = Vec::new();
1120 for catalog_name in self.context.catalog_names() {
1121 let Some(catalog) = self.context.catalog(&catalog_name) else {
1122 continue;
1123 };
1124 for schema_name in catalog.schema_names() {
1125 let Some(schema) = catalog.schema(&schema_name) else {
1126 continue;
1127 };
1128 names.extend(schema.table_names());
1129 }
1130 }
1131 names.sort();
1132 names.dedup();
1133 names
1134 }
1135
1136 fn make_sql_df(&self, name: &str, dataframe: DataFusionDataFrame) -> SqlDataFrame {
1139 SqlDataFrame::new(name, dataframe, self.table_row_counts())
1140 .with_context(self.context.clone())
1141 }
1142
1143 fn attach_query_metadata(&self, df: SqlDataFrame, query: &str) -> SqlDataFrame {
1145 let kind = if self.is_streaming_query(query).unwrap_or(false) {
1146 ExecutionKind::Streaming
1147 } else {
1148 ExecutionKind::Batch
1149 };
1150 df.with_query(query).with_execution_kind(kind)
1151 }
1152
1153 #[must_use]
1158 pub fn with_shuffle_partitions(self, n: Option<u32>) -> Self {
1159 if let Ok(mut guard) = self.shuffle_partitions.write() {
1160 *guard = n;
1161 }
1162 self
1163 }
1164
1165 fn build_local(
1175 krishiv_catalog: Option<Arc<RwLock<InMemoryCatalog>>>,
1176 window_fn_registration: WindowFnRegistration,
1177 target_partitions: NonZeroUsize,
1178 engine_memory: EngineMemory,
1179 ) -> SqlResult<Self> {
1180 let memory_limit_bytes = engine_memory.sizing_bytes();
1181 let streaming_sources: Arc<RwLock<std::collections::HashSet<String>>> =
1185 Arc::new(RwLock::new(std::collections::HashSet::new()));
1186
1187 let mut state_builder = with_krishiv_optimizer_rules(
1188 datafusion::execution::session_state::SessionStateBuilder::new()
1189 .with_default_features(),
1190 )
1191 .with_config(build_single_node_session_config(
1192 target_partitions,
1193 memory_limit_bytes,
1194 ));
1195 {
1196 let mut runtime_builder = datafusion::execution::runtime_env::RuntimeEnvBuilder::new()
1202 .with_object_store_registry(Arc::new(
1203 crate::object_store_registry::LazyCloudObjectStoreRegistry::new(),
1204 ));
1205 if let Some(pool) = engine_memory.pool() {
1206 runtime_builder = runtime_builder.with_memory_pool(pool);
1214 }
1215 let runtime_env = runtime_builder
1216 .build_arc()
1217 .map_err(|e| SqlError::DataFusion {
1218 message: format!(
1219 "failed to build DataFusion runtime \
1220 (memory limit {memory_limit_bytes:?} bytes): {e}"
1221 ),
1222 })?;
1223 state_builder = state_builder.with_runtime_env(runtime_env);
1224 }
1225 let mut state = state_builder.build();
1226 crate::connector_table::register_connector_table_factories(
1230 state.table_factories_mut(),
1231 streaming_sources.clone(),
1232 );
1233 let context = SessionContext::new_with_state(state);
1234 if let Some(catalog) = &krishiv_catalog {
1235 context.register_catalog(
1236 "krishiv",
1237 Arc::new(DataFusionCatalogBridge::new(catalog.clone())),
1238 );
1239 }
1240 if matches!(window_fn_registration, WindowFnRegistration::Register) {
1241 window_functions::register_window_functions(&context).map_err(|e| {
1242 SqlError::DataFusion {
1243 message: format!("failed to register window helper UDFs: {e}"),
1244 }
1245 })?;
1246 }
1247 json_functions::register_json_functions(&context).map_err(|e| SqlError::DataFusion {
1250 message: format!("failed to register JSON UDFs: {e}"),
1251 })?;
1252 higher_order_functions::register_higher_order_spark_functions(&context).map_err(|e| {
1255 SqlError::DataFusion {
1256 message: format!("failed to register higher-order UDFs: {e}"),
1257 }
1258 })?;
1259 spark_functions::register_spark_scalar_functions(&context).map_err(|e| {
1261 SqlError::DataFusion {
1262 message: format!("failed to register Spark scalar UDFs: {e}"),
1263 }
1264 })?;
1265 Ok(Self {
1266 context,
1267 target_parallelism: target_partitions,
1268 krishiv_catalog,
1269 udf_registry: None,
1270 streaming_sources,
1271 streaming_registration: Arc::new(Mutex::new(())),
1272 has_streaming_sources: Arc::new(AtomicBool::new(false)),
1273 udf_limits: None,
1274 udf_registry_version: Arc::new(AtomicU64::new(0)),
1275 udf_last_synced_version: Arc::new(AtomicU64::new(u64::MAX)),
1276 plan_cache: Arc::new(Mutex::new(PlanCache::new(resolve_plan_cache_max_entries()))),
1277 shuffle_partitions: Arc::new(std::sync::RwLock::new(None)),
1278 table_row_counts: Arc::new(std::sync::RwLock::new(HashMap::new())),
1279 memory_limit_bytes,
1280 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
1281 iceberg_catalogs: Arc::new(std::sync::RwLock::new(Vec::new())),
1282 live_table_registry: Arc::new(live_table::LiveTableRegistry::new()),
1283 incremental_view_registry: Arc::new(incremental_view::IncrementalViewRegistry::new()),
1284 pipeline_registry: Arc::new(pipeline_ddl::PipelineRegistry::new()),
1285 operation_registry: Arc::new(OperationRegistry::new()),
1286 })
1287 }
1288
1289 fn build_absolute_minimal(target_partitions: NonZeroUsize) -> Self {
1293 let streaming_sources: Arc<RwLock<std::collections::HashSet<String>>> =
1294 Arc::new(RwLock::new(std::collections::HashSet::new()));
1295 let mut state = with_krishiv_optimizer_rules(
1296 datafusion::execution::session_state::SessionStateBuilder::new()
1297 .with_default_features(),
1298 )
1299 .with_config(build_single_node_session_config(target_partitions, None))
1300 .build();
1301 crate::connector_table::register_connector_table_factories(
1302 state.table_factories_mut(),
1303 streaming_sources.clone(),
1304 );
1305 let context = SessionContext::new_with_state(state);
1306 Self {
1307 context,
1308 target_parallelism: target_partitions,
1309 krishiv_catalog: None,
1310 udf_registry: None,
1311 streaming_sources,
1312 streaming_registration: Arc::new(Mutex::new(())),
1313 has_streaming_sources: Arc::new(AtomicBool::new(false)),
1314 udf_limits: None,
1315 udf_registry_version: Arc::new(AtomicU64::new(0)),
1316 udf_last_synced_version: Arc::new(AtomicU64::new(u64::MAX)),
1317 plan_cache: Arc::new(Mutex::new(PlanCache::new(resolve_plan_cache_max_entries()))),
1318 shuffle_partitions: Arc::new(std::sync::RwLock::new(None)),
1319 table_row_counts: Arc::new(std::sync::RwLock::new(HashMap::new())),
1320 memory_limit_bytes: None,
1321 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
1322 iceberg_catalogs: Arc::new(std::sync::RwLock::new(Vec::new())),
1323 live_table_registry: Arc::new(live_table::LiveTableRegistry::new()),
1324 incremental_view_registry: Arc::new(incremental_view::IncrementalViewRegistry::new()),
1325 pipeline_registry: Arc::new(pipeline_ddl::PipelineRegistry::new()),
1326 operation_registry: Arc::new(OperationRegistry::new()),
1327 }
1328 }
1329
1330 pub fn register_streaming_table(
1341 &self,
1342 name: &str,
1343 schema: arrow::datatypes::SchemaRef,
1344 ) -> SqlResult<Arc<ContinuousTableInput>> {
1345 let _registration = self.lock_streaming_registration()?;
1346 self.validate_new_streaming_table(name, &schema)?;
1347 let (table, input) = crate::streaming::create_continuous_table(schema).map_err(|e| {
1348 SqlError::DataFusion {
1349 message: e.to_string(),
1350 }
1351 })?;
1352 self.register_new_streaming_provider(name, table)?;
1353 self.streaming_sources
1354 .write()
1355 .unwrap_or_else(|e| e.into_inner())
1356 .insert(name.to_string());
1357 self.has_streaming_sources.store(true, Ordering::Release);
1358 self.invalidate_plan_cache();
1359 Ok(input)
1360 }
1361
1362 pub fn register_streaming_table_with_capacity(
1367 &self,
1368 name: &str,
1369 schema: arrow::datatypes::SchemaRef,
1370 capacity: usize,
1371 ) -> SqlResult<Arc<ContinuousTableInput>> {
1372 let _registration = self.lock_streaming_registration()?;
1373 self.validate_new_streaming_table(name, &schema)?;
1374 let (table, input) = crate::streaming::create_continuous_table_with_capacity(
1375 schema, capacity,
1376 )
1377 .map_err(|e| SqlError::DataFusion {
1378 message: e.to_string(),
1379 })?;
1380 self.register_new_streaming_provider(name, table)?;
1381 self.streaming_sources
1382 .write()
1383 .unwrap_or_else(|e| e.into_inner())
1384 .insert(name.to_string());
1385 self.has_streaming_sources.store(true, Ordering::Release);
1386 self.invalidate_plan_cache();
1387 Ok(input)
1388 }
1389
1390 fn lock_streaming_registration(&self) -> SqlResult<std::sync::MutexGuard<'_, ()>> {
1391 self.streaming_registration
1392 .lock()
1393 .map_err(|error| SqlError::DataFusion {
1394 message: format!("streaming table registration lock poisoned: {error}"),
1395 })
1396 }
1397
1398 fn validate_new_streaming_table(
1399 &self,
1400 name: &str,
1401 schema: &arrow::datatypes::SchemaRef,
1402 ) -> SqlResult<()> {
1403 if name.trim().is_empty() {
1404 return Err(SqlError::EmptyTableName);
1405 }
1406 if schema.fields().is_empty() {
1407 return Err(SqlError::DataFusion {
1408 message: "streaming table schema must contain at least one field".into(),
1409 });
1410 }
1411 if self
1412 .context
1413 .table_exist(name)
1414 .map_err(|error| SqlError::DataFusion {
1415 message: error.to_string(),
1416 })?
1417 {
1418 return Err(SqlError::DataFusion {
1419 message: format!("table '{name}' is already registered"),
1420 });
1421 }
1422 Ok(())
1423 }
1424
1425 fn register_new_streaming_provider(
1426 &self,
1427 name: &str,
1428 table: Arc<dyn datafusion::catalog::TableProvider>,
1429 ) -> SqlResult<()> {
1430 let previous =
1431 self.context
1432 .register_table(name, table)
1433 .map_err(|error| SqlError::DataFusion {
1434 message: error.to_string(),
1435 })?;
1436 if let Some(previous) = previous {
1437 self.context
1438 .register_table(name, previous)
1439 .map_err(|error| SqlError::DataFusion {
1440 message: format!(
1441 "table '{name}' was concurrently registered and could not be restored: \
1442 {error}"
1443 ),
1444 })?;
1445 return Err(SqlError::DataFusion {
1446 message: format!("table '{name}' was concurrently registered"),
1447 });
1448 }
1449 Ok(())
1450 }
1451
1452 pub fn register_kafka_source(
1466 &self,
1467 table_name: impl AsRef<str>,
1468 schema: arrow::datatypes::SchemaRef,
1469 bootstrap_servers: impl Into<String>,
1470 topic: impl Into<String>,
1471 group_id: impl Into<String>,
1472 ) -> SqlResult<()> {
1473 let table_name = table_name.as_ref();
1474 if table_name.trim().is_empty() {
1475 return Err(SqlError::EmptyTableName);
1476 }
1477 let config = krishiv_connectors::kafka::KafkaConfig {
1478 bootstrap_servers: bootstrap_servers.into(),
1479 topic: topic.into(),
1480 group_id: group_id.into(),
1481 auto_commit_interval_ms: {
1482 let profile = krishiv_common::resolve_durability_profile();
1483 if krishiv_common::requires_manual_kafka_commit(profile) {
1484 None
1485 } else {
1486 Some(1_000)
1487 }
1488 },
1489 security_protocol: None,
1490 ssl_ca_location: None,
1491 ssl_certificate_location: None,
1492 ssl_key_location: None,
1493 ssl_key_password: None,
1494 sasl_username: None,
1495 sasl_password: None,
1496 sasl_mechanisms: None,
1497 enable_idempotence: None,
1498 transactional_id: None,
1499 };
1500 let table =
1501 crate::kafka_table::create_kafka_streaming_table(schema, config).map_err(|e| {
1502 SqlError::DataFusion {
1503 message: e.to_string(),
1504 }
1505 })?;
1506 if self
1507 .context
1508 .table_exist(table_name)
1509 .map_err(SqlError::from)?
1510 {
1511 let _ = self
1512 .context
1513 .deregister_table(table_name)
1514 .map_err(SqlError::from)?;
1515 }
1516 self.context
1517 .register_table(table_name, table)
1518 .map_err(|e| SqlError::DataFusion {
1519 message: e.to_string(),
1520 })?;
1521 self.streaming_sources
1522 .write()
1523 .unwrap_or_else(|e| e.into_inner())
1524 .insert(table_name.to_string());
1525 self.has_streaming_sources.store(true, Ordering::Release);
1526 self.invalidate_plan_cache();
1527 Ok(())
1528 }
1529
1530 pub async fn sql_to_kafka(
1540 &self,
1541 sql: impl AsRef<str>,
1542 bootstrap_servers: impl Into<String>,
1543 topic: impl Into<String>,
1544 ) -> SqlResult<u64> {
1545 use futures::StreamExt;
1546 use krishiv_connectors::Sink as _;
1547 use krishiv_connectors::kafka::{KafkaConfig, KafkaSink};
1548
1549 let config = KafkaConfig {
1550 bootstrap_servers: bootstrap_servers.into(),
1551 topic: topic.into(),
1552 group_id: "krishiv-sql-writer".into(),
1553 auto_commit_interval_ms: None,
1554 security_protocol: None,
1555 ssl_ca_location: None,
1556 ssl_certificate_location: None,
1557 ssl_key_location: None,
1558 ssl_key_password: None,
1559 sasl_username: None,
1560 sasl_password: None,
1561 sasl_mechanisms: None,
1562 enable_idempotence: None,
1563 transactional_id: None,
1564 };
1565 let mut sink = KafkaSink::new(config).map_err(|e| SqlError::DataFusion {
1566 message: e.to_string(),
1567 })?;
1568
1569 let df = self.sql(sql.as_ref()).await?;
1570 let mut stream = df.execute_stream().await?;
1571 let mut total_rows = 0u64;
1572
1573 while let Some(result) = stream.next().await {
1574 let batch = result.map_err(|e| SqlError::DataFusion {
1575 message: e.to_string(),
1576 })?;
1577 if batch.num_rows() > 0 {
1578 total_rows += batch.num_rows() as u64;
1579 sink.write_batch(batch)
1580 .await
1581 .map_err(|e| SqlError::DataFusion {
1582 message: e.to_string(),
1583 })?;
1584 }
1585 }
1586 sink.flush().await.map_err(|e| SqlError::DataFusion {
1587 message: e.to_string(),
1588 })?;
1589 Ok(total_rows)
1590 }
1591
1592 pub fn with_udf_limits(mut self, limits: krishiv_plan::udf::ResourceLimits) -> Self {
1596 self.udf_limits = Some(limits);
1597 self
1598 }
1599
1600 pub fn is_streaming_source(&self, table_name: &str) -> bool {
1602 self.streaming_sources
1603 .read()
1604 .unwrap_or_else(|e| e.into_inner())
1605 .contains(table_name)
1606 }
1607
1608 pub fn register_streaming_source_name(&self, table_name: impl Into<String>) -> SqlResult<()> {
1617 let name: String = table_name.into();
1618 if name.trim().is_empty() {
1619 return Err(SqlError::EmptyTableName);
1620 }
1621 self.streaming_sources
1622 .write()
1623 .unwrap_or_else(|e| e.into_inner())
1624 .insert(name);
1625 self.has_streaming_sources.store(true, Ordering::Release);
1626 self.invalidate_plan_cache();
1627 Ok(())
1628 }
1629
1630 pub fn deregister_streaming_source(&self, name: &str) -> SqlResult<()> {
1636 if name.trim().is_empty() {
1637 return Err(SqlError::EmptyTableName);
1638 }
1639 let _ = self
1641 .context
1642 .deregister_table(name)
1643 .map_err(SqlError::from)?;
1644 {
1645 let mut sources = self
1646 .streaming_sources
1647 .write()
1648 .unwrap_or_else(|e| e.into_inner());
1649 sources.remove(name);
1650 if sources.is_empty() {
1651 self.has_streaming_sources.store(false, Ordering::Release);
1652 }
1653 self.invalidate_plan_cache();
1657 }
1658 Ok(())
1659 }
1660
1661 pub fn live_table_registry(&self) -> &Arc<live_table::LiveTableRegistry> {
1663 &self.live_table_registry
1664 }
1665
1666 pub fn incremental_view_registry(&self) -> &Arc<incremental_view::IncrementalViewRegistry> {
1668 &self.incremental_view_registry
1669 }
1670
1671 pub fn pipeline_registry(&self) -> &Arc<pipeline_ddl::PipelineRegistry> {
1673 &self.pipeline_registry
1674 }
1675
1676 pub fn operation_registry(&self) -> &Arc<OperationRegistry> {
1678 &self.operation_registry
1679 }
1680
1681 pub fn deregister_table(&self, name: &str) -> SqlResult<()> {
1700 if name.trim().is_empty() {
1701 return Err(SqlError::EmptyTableName);
1702 }
1703 let _ = self
1704 .context
1705 .deregister_table(name)
1706 .map_err(SqlError::from)?;
1707 {
1708 let mut sources = self
1709 .streaming_sources
1710 .write()
1711 .unwrap_or_else(|e| e.into_inner());
1712 sources.remove(name);
1713 if sources.is_empty() {
1714 self.has_streaming_sources.store(false, Ordering::Release);
1715 }
1716 self.invalidate_plan_cache();
1721 }
1722 Ok(())
1723 }
1724
1725 pub fn register_table_udf_fn(
1749 &self,
1750 name: impl Into<String>,
1751 schema: arrow::datatypes::Schema,
1752 f: impl Fn(
1753 &[krishiv_plan::udf::ScalarValue],
1754 ) -> Result<arrow::record_batch::RecordBatch, krishiv_plan::udf::UdfError>
1755 + Send
1756 + Sync
1757 + 'static,
1758 ) -> SqlResult<()> {
1759 let udf =
1760 create_function_ddl::ClosureTableUdf::try_new(name, schema, std::sync::Arc::new(f))
1761 .map_err(|error| SqlError::InvalidTableFunction {
1762 message: error.to_string(),
1763 })?;
1764 if let Some(registry) = &self.udf_registry {
1765 let mut guard = registry.write().map_err(|e| SqlError::DataFusion {
1766 message: e.to_string(),
1767 })?;
1768 guard.register_table(std::sync::Arc::new(udf.clone()));
1769 }
1770 udf::register_single_table_udf(&self.context, std::sync::Arc::new(udf))
1771 .map_err(SqlError::from)
1772 }
1773
1774 pub fn is_streaming_query(&self, sql: &str) -> SqlResult<bool> {
1776 if !self.has_streaming_sources.load(Ordering::Acquire) {
1779 return Ok(false);
1780 }
1781 let sources = self
1782 .streaming_sources
1783 .read()
1784 .unwrap_or_else(|e| e.into_inner());
1785 if sources.is_empty() {
1786 return Ok(false);
1787 }
1788 let dialect = GenericDialect {};
1789 let statements = Parser::parse_sql(&dialect, sql).map_err(|e| SqlError::DataFusion {
1790 message: e.to_string(),
1791 })?;
1792 for stmt in &statements {
1793 let mut is_streaming = false;
1794 let _ = visit_relations(stmt, |relation| {
1795 let full = relation.to_string();
1798 let table_name = full.split('.').next_back().unwrap_or(&full);
1799 if sources.contains(table_name) {
1800 is_streaming = true;
1801 return ControlFlow::Break(());
1802 }
1803 ControlFlow::Continue(())
1804 });
1805 if is_streaming {
1806 return Ok(true);
1807 }
1808 }
1809 Ok(false)
1810 }
1811
1812 pub fn krishiv_catalog(&self) -> Option<&Arc<RwLock<InMemoryCatalog>>> {
1814 self.krishiv_catalog.as_ref()
1815 }
1816
1817 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
1826 #[must_use]
1827 pub fn with_iceberg_catalog(
1828 self,
1829 catalog: std::sync::Arc<catalog::unified::KrishivCatalog>,
1830 catalog_name: impl Into<String>,
1831 ) -> Self {
1832 self.register_iceberg_catalog(catalog, catalog_name);
1833 self
1834 }
1835
1836 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
1843 pub fn register_iceberg_catalog(
1844 &self,
1845 catalog: std::sync::Arc<catalog::unified::KrishivCatalog>,
1846 catalog_name: impl Into<String>,
1847 ) {
1848 let catalog_name = catalog_name.into();
1849 let bridge = catalog::iceberg_catalog_bridge::IcebergCatalogBridge::new(
1850 Arc::clone(&catalog),
1851 catalog_name.clone(),
1852 );
1853 self.context
1854 .register_catalog(catalog_name.clone(), Arc::new(bridge));
1855 self.iceberg_catalogs
1856 .write()
1857 .unwrap_or_else(|e| e.into_inner())
1858 .push((catalog, catalog_name));
1859 self.invalidate_plan_cache();
1860 }
1861
1862 pub async fn register_iceberg_rest_catalog_from_env(&self) -> Result<bool, String> {
1874 #[cfg(feature = "rest-catalog")]
1875 {
1876 let uri = match std::env::var("KRISHIV_ICEBERG_REST_URI") {
1877 Ok(uri) => uri,
1878 Err(_) => return Ok(false),
1879 };
1880 let warehouse = std::env::var("KRISHIV_ICEBERG_REST_WAREHOUSE").unwrap_or_default();
1881 let token = std::env::var("KRISHIV_ICEBERG_REST_TOKEN").ok();
1882 let name =
1887 std::env::var("KRISHIV_ICEBERG_REST_NAME").unwrap_or_else(|_| String::from("main"));
1888 self.register_s3_object_store_for_warehouse(&warehouse)?;
1894 let catalog = std::sync::Arc::new(
1895 catalog::unified::KrishivCatalog::rest(&uri, &warehouse, token.as_deref())
1896 .await
1897 .map_err(|e| format!("iceberg REST catalog at {uri}: {e}"))?,
1898 );
1899 self.register_iceberg_catalog(std::sync::Arc::clone(&catalog), &name);
1900 if name != "krishiv" {
1906 self.register_iceberg_catalog(catalog, "krishiv");
1907 }
1908 Ok(true)
1909 }
1910 #[cfg(not(feature = "rest-catalog"))]
1911 {
1912 Ok(false)
1913 }
1914 }
1915
1916 #[must_use]
1918 pub fn with_udf_registry(
1919 mut self,
1920 registry: std::sync::Arc<std::sync::RwLock<krishiv_plan::udf::UdfRegistry>>,
1921 ) -> Self {
1922 self.udf_registry = Some(registry);
1923 self.bump_udf_version();
1925 self
1926 }
1927
1928 pub(crate) fn bump_udf_version(&self) {
1931 self.udf_registry_version.fetch_add(1, Ordering::Release);
1932 }
1933
1934 fn invalidate_plan_cache(&self) {
1939 match self.plan_cache.lock() {
1940 Ok(mut cache) => cache.clear(),
1941 Err(poisoned) => poisoned.into_inner().clear(),
1942 }
1943 }
1944
1945 pub fn clear_plan_cache(&self) {
1948 self.invalidate_plan_cache();
1949 }
1950
1951 pub async fn register_python_udfs_from_sql(&self, sql: &str) -> SqlResult<String> {
1962 const SCALAR_PREFIX: &str = "/* krishiv-register-python-udf:";
1963 const AGG_PREFIX: &str = "/* krishiv-register-python-udaf:";
1964 if !sql.contains(SCALAR_PREFIX) && !sql.contains(AGG_PREFIX) {
1965 return Ok(sql.to_string());
1966 }
1967 let mut out = String::with_capacity(sql.len());
1968 let mut rest = sql;
1969 loop {
1970 let agg = rest.find(AGG_PREFIX).map(|i| (i, true, AGG_PREFIX.len()));
1973 let scalar = rest
1974 .find(SCALAR_PREFIX)
1975 .map(|i| (i, false, SCALAR_PREFIX.len()));
1976 let next = match (agg, scalar) {
1977 (Some(a), Some(s)) => Some(if a.0 <= s.0 { a } else { s }),
1978 (Some(a), None) => Some(a),
1979 (None, Some(s)) => Some(s),
1980 (None, None) => None,
1981 };
1982 let Some((start, is_aggregate, prefix_len)) = next else {
1983 break;
1984 };
1985 out.push_str(&rest[..start]);
1986 let after = &rest[start + prefix_len..];
1987 let Some(end) = after.find(" */") else {
1988 out.push_str(&rest[start..]);
1990 return Ok(out);
1991 };
1992 self.register_python_udf_directive(&after[..end], is_aggregate)
1993 .await?;
1994 rest = &after[end + " */".len()..];
1995 }
1996 out.push_str(rest);
1997 Ok(out)
1998 }
1999
2000 async fn register_python_udf_directive(&self, body: &str, is_aggregate: bool) -> SqlResult<()> {
2003 use base64::Engine as _;
2004 let mut parts = body.splitn(4, ':');
2005 let (name, in_types, out_type, pickle_b64) =
2006 match (parts.next(), parts.next(), parts.next(), parts.next()) {
2007 (Some(n), Some(i), Some(o), Some(p)) => (n, i, o, p),
2008 _ => {
2009 return Err(SqlError::DataFusion {
2010 message: "malformed python-udf directive".into(),
2011 });
2012 }
2013 };
2014 let input_types: Vec<String> = if in_types.is_empty() {
2015 Vec::new()
2016 } else {
2017 in_types.split(',').map(str::to_string).collect()
2018 };
2019 let pickle = base64::engine::general_purpose::STANDARD
2020 .decode(pickle_b64)
2021 .map_err(|e| SqlError::DataFusion {
2022 message: format!("invalid python-udf pickle base64: {e}"),
2023 })?;
2024 if is_aggregate {
2025 self.register_python_udaf(name, &pickle, &input_types, out_type)
2026 .await
2027 } else {
2028 self.register_python_udf(name, &pickle, &input_types, out_type)
2029 .await
2030 }
2031 }
2032
2033 pub async fn register_python_udf(
2039 &self,
2040 name: &str,
2041 pickle: &[u8],
2042 input_types: &[String],
2043 output_type: &str,
2044 ) -> SqlResult<()> {
2045 use arrow::datatypes::{Field, Schema};
2046 let Some(registry) = &self.udf_registry else {
2047 return Err(SqlError::DataFusion {
2048 message: "cannot register a python UDF: engine has no UDF registry".into(),
2049 });
2050 };
2051 let input_fields: Vec<Field> = input_types
2052 .iter()
2053 .enumerate()
2054 .map(|(i, t)| Field::new(format!("a{i}"), python_udf_arrow_type(t), true))
2055 .collect();
2056 let input_schema = Schema::new(input_fields);
2057 let output_field = Field::new("out", python_udf_arrow_type(output_type), true);
2058 let pool = crate::python_udf::global_pool().map_err(|e| SqlError::DataFusion {
2059 message: format!("python UDF worker unavailable: {e:?}"),
2060 })?;
2061 let udf = std::sync::Arc::new(crate::python_udf::PythonWorkerUdf::new(
2062 name,
2063 pickle.to_vec(),
2064 input_schema,
2065 output_field,
2066 pool,
2067 ));
2068 registry
2069 .write()
2070 .map_err(|e| SqlError::DataFusion {
2071 message: e.to_string(),
2072 })?
2073 .register_scalar(udf);
2074 self.udf_registry_version
2075 .fetch_add(1, std::sync::atomic::Ordering::Release);
2076 self.sync_scalar_udfs().await
2077 }
2078
2079 pub async fn register_python_udaf(
2086 &self,
2087 name: &str,
2088 pickle: &[u8],
2089 input_types: &[String],
2090 output_type: &str,
2091 ) -> SqlResult<()> {
2092 use arrow::datatypes::{Field, Schema};
2093 let Some(registry) = &self.udf_registry else {
2094 return Err(SqlError::DataFusion {
2095 message: "cannot register a python UDAF: engine has no UDF registry".into(),
2096 });
2097 };
2098 let input_fields: Vec<Field> = input_types
2099 .iter()
2100 .enumerate()
2101 .map(|(i, t)| Field::new(format!("a{i}"), python_udf_arrow_type(t), true))
2102 .collect();
2103 let input_schema = Schema::new(input_fields);
2104 let output_field = Field::new("out", python_udf_arrow_type(output_type), true);
2105 let pool = crate::python_udf::global_pool().map_err(|e| SqlError::DataFusion {
2106 message: format!("python UDF worker unavailable: {e:?}"),
2107 })?;
2108 let udf = std::sync::Arc::new(crate::python_udf::PythonWorkerAggregateUdf::new(
2109 name,
2110 pickle.to_vec(),
2111 input_schema,
2112 output_field,
2113 pool,
2114 ));
2115 registry
2116 .write()
2117 .map_err(|e| SqlError::DataFusion {
2118 message: e.to_string(),
2119 })?
2120 .register_aggregate(udf);
2121 self.udf_registry_version
2122 .fetch_add(1, std::sync::atomic::Ordering::Release);
2123 self.sync_aggregate_udfs().await
2124 }
2125
2126 pub async fn sync_scalar_udfs(&self) -> SqlResult<()> {
2127 let Some(registry) = &self.udf_registry else {
2128 return Ok(());
2129 };
2130 let guard = registry.read().map_err(|e| SqlError::DataFusion {
2131 message: e.to_string(),
2132 })?;
2133 let limits = self.udf_limits.clone().unwrap_or_default();
2134 udf::sync_scalar_udfs_with_limits(&self.context, &guard, limits).map_err(|e| {
2135 SqlError::DataFusion {
2136 message: e.to_string(),
2137 }
2138 })
2139 }
2140
2141 pub async fn sync_scalar_udfs_with_limits(
2146 &self,
2147 limits: krishiv_plan::udf::ResourceLimits,
2148 ) -> SqlResult<()> {
2149 self.sync_scalar_udfs_with_limits_for_profile(
2150 limits,
2151 krishiv_common::resolve_durability_profile(),
2152 )
2153 .await
2154 }
2155
2156 pub async fn sync_scalar_udfs_with_limits_for_profile(
2158 &self,
2159 limits: krishiv_plan::udf::ResourceLimits,
2160 profile: krishiv_common::DurabilityProfile,
2161 ) -> SqlResult<()> {
2162 self.sync_scalar_udfs_with_limits_for_policy(
2163 limits,
2164 krishiv_common::NativeScalarUdfPolicy::resolve(profile),
2165 )
2166 .await
2167 }
2168
2169 pub async fn sync_scalar_udfs_with_limits_for_policy(
2171 &self,
2172 limits: krishiv_plan::udf::ResourceLimits,
2173 policy: krishiv_common::NativeScalarUdfPolicy,
2174 ) -> SqlResult<()> {
2175 let Some(registry) = &self.udf_registry else {
2176 return Ok(());
2177 };
2178 let guard = registry.read().map_err(|e| SqlError::DataFusion {
2179 message: e.to_string(),
2180 })?;
2181 udf::sync_scalar_udfs_with_limits_for_policy(&self.context, &guard, limits, policy).map_err(
2182 |e| SqlError::DataFusion {
2183 message: e.to_string(),
2184 },
2185 )
2186 }
2187
2188 pub async fn sync_aggregate_udfs(&self) -> SqlResult<()> {
2190 let Some(registry) = &self.udf_registry else {
2191 return Ok(());
2192 };
2193 let guard = registry.read().map_err(|e| SqlError::DataFusion {
2194 message: e.to_string(),
2195 })?;
2196 udf::sync_aggregate_udfs(&self.context, &guard).map_err(|e| SqlError::DataFusion {
2197 message: e.to_string(),
2198 })
2199 }
2200
2201 pub async fn sync_table_udfs(&self) -> SqlResult<()> {
2203 let Some(registry) = &self.udf_registry else {
2204 return Ok(());
2205 };
2206 let guard = registry.read().map_err(|e| SqlError::DataFusion {
2207 message: e.to_string(),
2208 })?;
2209 udf::sync_table_udfs(&self.context, &guard).map_err(|e| SqlError::DataFusion {
2210 message: e.to_string(),
2211 })
2212 }
2213
2214 pub async fn sync_all_udfs(&self) -> SqlResult<()> {
2216 self.sync_scalar_udfs().await?;
2217 self.sync_aggregate_udfs().await?;
2218 self.sync_table_udfs().await?;
2219 Ok(())
2220 }
2221
2222 pub(crate) fn register_s3_object_store_for_warehouse(&self, path: &str) -> Result<(), String> {
2229 if !(path.starts_with("s3://") || path.starts_with("s3a://")) {
2230 return Ok(());
2231 }
2232 let url = url::Url::parse(path).map_err(|e| format!("invalid s3 url {path}: {e}"))?;
2233 let bucket = url.host_str().unwrap_or_default();
2234 let store_url = url::Url::parse(&format!("s3://{bucket}"))
2236 .map_err(|e| format!("invalid s3 bucket url: {e}"))?;
2237 let store = build_s3_object_store(bucket).map_err(|e| format!("s3 store init: {e}"))?;
2238 self.context.register_object_store(&store_url, store);
2239 Ok(())
2240 }
2241
2242 pub async fn register_parquet(
2244 &self,
2245 table_name: impl AsRef<str>,
2246 path: impl AsRef<Path>,
2247 ) -> SqlResult<()> {
2248 self.register_parquet_with_primary_key(table_name, path, &[] as &[String])
2249 .await
2250 }
2251
2252 pub async fn register_parquet_with_primary_key<S: AsRef<str>>(
2270 &self,
2271 table_name: impl AsRef<str>,
2272 path: impl AsRef<Path>,
2273 primary_key: &[S],
2274 ) -> SqlResult<()> {
2275 let table_name = table_name.as_ref();
2276 if table_name.trim().is_empty() {
2277 return Err(SqlError::EmptyTableName);
2278 }
2279
2280 let path = path.as_ref().to_string_lossy().into_owned();
2281
2282 self.register_s3_object_store_for_warehouse(&path)
2285 .map_err(|message| SqlError::DataFusion { message })?;
2286
2287 if self
2288 .context
2289 .table_exist(table_name)
2290 .map_err(SqlError::from)?
2291 {
2292 let _ = self
2293 .context
2294 .deregister_table(table_name)
2295 .map_err(SqlError::from)?;
2296 }
2297 let spec = crate::distributed_plan::ParquetTableSpec::new(table_name, path)
2304 .with_primary_key(primary_key.iter().map(|c| c.as_ref().to_owned()));
2305 crate::distributed_plan::register_parquet_table(&self.context, &spec).await?;
2306 if let Ok(provider) = self.context.table_provider(table_name).await
2308 && let Some(stats) = provider.statistics()
2309 && let Some(n) = stats.num_rows.get_value()
2310 && let Ok(mut counts) = self.table_row_counts.write()
2311 {
2312 counts.insert(table_name.to_string(), *n as u64);
2313 }
2314 self.invalidate_plan_cache();
2315 Ok(())
2316 }
2317
2318 pub async fn read_parquet(&self, path: impl AsRef<Path>) -> SqlResult<SqlDataFrame> {
2320 let path = path.as_ref().to_string_lossy().into_owned();
2321 let dataframe = self
2322 .context
2323 .read_parquet(path, ParquetReadOptions::default())
2324 .await?;
2325 Ok(self.make_sql_df("parquet-read", dataframe))
2326 }
2327
2328 pub async fn register_record_batches(
2334 &self,
2335 table_name: impl AsRef<str>,
2336 batches: Vec<RecordBatch>,
2337 ) -> SqlResult<()> {
2338 use std::sync::Arc;
2339 let table_name = table_name.as_ref();
2340 if table_name.trim().is_empty() {
2341 return Err(SqlError::EmptyTableName);
2342 }
2343 if batches.is_empty() {
2344 return Ok(());
2345 }
2346 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
2347 let schema = batches
2348 .first()
2349 .ok_or_else(|| SqlError::DataFusion {
2350 message: "empty batch list".into(),
2351 })?
2352 .schema();
2353 let mem_table =
2354 datafusion::datasource::MemTable::try_new(schema, vec![batches]).map_err(|e| {
2355 SqlError::DataFusion {
2356 message: e.to_string(),
2357 }
2358 })?;
2359 if self
2360 .context
2361 .table_exist(table_name)
2362 .map_err(SqlError::from)?
2363 {
2364 let _ = self
2365 .context
2366 .deregister_table(table_name)
2367 .map_err(SqlError::from)?;
2368 }
2369 self.context
2370 .register_table(table_name, Arc::new(mem_table))
2371 .map_err(|e| SqlError::DataFusion {
2372 message: e.to_string(),
2373 })?;
2374 if total_rows > 0
2375 && let Ok(mut counts) = self.table_row_counts.write()
2376 {
2377 counts.insert(table_name.to_string(), total_rows as u64);
2378 }
2379 self.invalidate_plan_cache();
2380 Ok(())
2381 }
2382
2383 pub async fn read_parquet_with_options(
2385 &self,
2386 path: impl AsRef<Path>,
2387 opts: &ParquetReaderOptions,
2388 ) -> SqlResult<SqlDataFrame> {
2389 let path = path.as_ref().to_string_lossy().into_owned();
2390 let mut options = datafusion::prelude::ParquetReadOptions::default();
2391 if opts.batch_size.is_some() {
2392 options = options.parquet_pruning(true);
2393 }
2394 let dataframe = self.context.read_parquet(path, options).await?;
2400 Ok(self.make_sql_df("parquet-read", dataframe))
2401 }
2402
2403 pub async fn read_csv(&self, path: impl AsRef<Path>) -> SqlResult<SqlDataFrame> {
2405 self.read_csv_with_options(path, &CsvReaderOptions::default())
2406 .await
2407 }
2408
2409 pub async fn read_csv_with_options(
2411 &self,
2412 path: impl AsRef<Path>,
2413 opts: &CsvReaderOptions,
2414 ) -> SqlResult<SqlDataFrame> {
2415 let path = path.as_ref().to_string_lossy().into_owned();
2416 let mut options = datafusion::prelude::CsvReadOptions::new();
2417 if let Some(delim) = opts.delimiter {
2418 options = options.delimiter(delim as u8);
2419 }
2420 if let Some(has_header) = opts.has_header {
2421 options = options.has_header(has_header);
2422 }
2423 let dataframe = self.context.read_csv(path, options).await?;
2424 Ok(self.make_sql_df("csv-read", dataframe))
2425 }
2426
2427 pub async fn read_json(&self, path: impl AsRef<Path>) -> SqlResult<SqlDataFrame> {
2429 let path = path.as_ref().to_string_lossy().into_owned();
2430 let dataframe = self
2431 .context
2432 .read_json(path, datafusion::prelude::JsonReadOptions::default())
2433 .await?;
2434 Ok(self.make_sql_df("json-read", dataframe))
2435 }
2436
2437 pub async fn read_delta(
2439 &self,
2440 path: impl AsRef<str>,
2441 version: Option<i64>,
2442 ) -> SqlResult<SqlDataFrame> {
2443 let path = path.as_ref();
2444 let base = path.replace(['/', '.', '-'], "_");
2445 let table = match version {
2446 Some(v) => format!("delta_{base}_v{v}"),
2447 None => format!("delta_{base}"),
2448 };
2449 lakehouse::register_delta_uri(&self.context, &table, path, version).await?;
2450 self.sql(format!("SELECT * FROM {table}")).await
2451 }
2452
2453 pub async fn read_hudi(
2455 &self,
2456 path: impl AsRef<str>,
2457 query_type: krishiv_connectors::lakehouse::HudiQueryType,
2458 begin_instant: Option<&str>,
2459 ) -> SqlResult<SqlDataFrame> {
2460 let path = path.as_ref();
2461 let table = format!("hudi_{}", path.replace(['/', '.', '-'], "_"));
2462 lakehouse::register_hudi_uri(&self.context, &table, path, query_type, begin_instant)
2463 .await?;
2464 self.sql(format!("SELECT * FROM {table}")).await
2465 }
2466
2467 pub fn sql<'a>(
2477 &'a self,
2478 query: impl AsRef<str> + Send + 'a,
2479 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = SqlResult<SqlDataFrame>> + Send + 'a>>
2480 {
2481 let query: String = query.as_ref().to_owned();
2482 Box::pin(self.sql_boxed_body(query))
2483 }
2484
2485 async fn sql_boxed_body(&self, query: String) -> SqlResult<SqlDataFrame> {
2487 let query = query.as_str();
2488 if query.trim().is_empty() {
2489 return Err(SqlError::EmptyQuery);
2490 }
2491
2492 let script = split_sql_statements(query);
2501 if script.len() > 1
2502 && let [setup @ .., last_stmt] = script.as_slice()
2503 {
2504 for stmt in setup {
2505 Box::pin(self.sql(stmt.as_str())).await?.collect().await?;
2509 }
2510 return Box::pin(self.sql(last_stmt.as_str())).await;
2511 }
2512
2513 {
2517 let current = self.udf_registry_version.load(Ordering::Acquire);
2518 let last = self.udf_last_synced_version.load(Ordering::Relaxed);
2519 if current != last {
2520 self.sync_all_udfs().await?;
2521 self.udf_last_synced_version
2522 .store(current, Ordering::Release);
2523 }
2524 }
2525
2526 if let Some(stmt) = introspection_sql::parse_introspection_statement(query)? {
2528 return match stmt {
2529 introspection_sql::IntrospectionStatement::Describe { table } => {
2530 let batch = introspection_sql::describe_table(&self.context, &table).await?;
2531 let describe_table_name = next_ephemeral_name("describe_result");
2532 lakehouse::register_scan_batches(
2533 &self.context,
2534 &describe_table_name,
2535 vec![batch],
2536 )
2537 .await?;
2538 let dataframe = self
2539 .context
2540 .sql(&format!("SELECT * FROM {describe_table_name}"))
2541 .await?;
2542 Ok(self.attach_query_metadata(self.make_sql_df("describe", dataframe), query))
2543 }
2544 introspection_sql::IntrospectionStatement::Explain { mode, query: inner } => {
2545 let text = introspection_sql::explain_query(&inner, mode)?;
2546 let batch = introspection_sql::explain_result_batch(&text)?;
2547 let explain_table = next_ephemeral_name("explain_result");
2548 lakehouse::register_scan_batches(&self.context, &explain_table, vec![batch])
2549 .await?;
2550 let dataframe = self
2551 .context
2552 .sql(&format!("SELECT * FROM {explain_table}"))
2553 .await?;
2554 Ok(self.attach_query_metadata(self.make_sql_df("explain", dataframe), query))
2555 }
2556 };
2557 }
2558
2559 if live_table::execute_live_table_ddl(&self.live_table_registry, query)?.is_some() {
2561 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2562 return Ok(self.attach_query_metadata(self.make_sql_df("live-table-ddl", empty), query));
2563 }
2564
2565 match incremental_view::execute_incremental_view_ddl(
2567 &self.incremental_view_registry,
2568 query,
2569 )? {
2570 Some(incremental_view::IncrementalViewResult::Refresh(_name)) => {
2571 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2574 return Ok(self.attach_query_metadata(
2575 self.make_sql_df("incremental-view-refresh", empty),
2576 query,
2577 ));
2578 }
2579 Some(_) => {
2580 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2581 return Ok(self.attach_query_metadata(
2582 self.make_sql_df("incremental-view-ddl", empty),
2583 query,
2584 ));
2585 }
2586 None => {}
2587 }
2588
2589 if let Some(ddl) = streaming_table_ddl::parse_create_streaming_table(query) {
2597 let _plan = streaming_window_plan::compile_streaming_window_sql(&ddl.query)?;
2598 return Err(SqlError::Unsupported {
2599 feature: format!(
2600 "CREATE STREAMING TABLE '{}' compiled to a continuous plan, but this session \
2601 has no streaming coordinator to run it; submit it via the continuous-stream \
2602 registration API or a cluster-attached session",
2603 ddl.name
2604 ),
2605 });
2606 }
2607
2608 if pipeline_ddl::execute_pipeline_ddl(&self.pipeline_registry, query)?.is_some() {
2612 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2613 return Ok(self.attach_query_metadata(self.make_sql_df("pipeline-ddl", empty), query));
2614 }
2615
2616 let trimmed = query.trim();
2619 if trimmed
2620 .to_ascii_uppercase()
2621 .starts_with("SET SHUFFLE.PARTITIONS")
2622 {
2623 let value = trimmed.split('=').nth(1).map(|s| s.trim()).unwrap_or("");
2624 match value.parse::<u32>() {
2625 Ok(n) if n > 0 => {
2626 {
2627 let mut guard =
2628 self.shuffle_partitions
2629 .write()
2630 .map_err(|e| SqlError::DataFusion {
2631 message: e.to_string(),
2632 })?;
2633 *guard = Some(n);
2634 }
2635 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2636 return Ok(self.make_sql_df("set-shuffle-partitions", empty));
2637 }
2638 Ok(_) => {
2639 {
2640 let mut guard =
2641 self.shuffle_partitions
2642 .write()
2643 .map_err(|e| SqlError::DataFusion {
2644 message: e.to_string(),
2645 })?;
2646 *guard = None;
2647 }
2648 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2649 return Ok(self.make_sql_df("set-shuffle-partitions", empty));
2650 }
2651 Err(_) => {
2652 return Err(SqlError::DataFusion {
2653 message: format!(
2654 "invalid shuffle.partitions value '{value}'; expected a positive integer"
2655 ),
2656 });
2657 }
2658 }
2659 }
2660
2661 if let Some(result) = statement_completion::apply_use(&self.context, query) {
2665 result.map_err(|message| SqlError::DataFusion { message })?;
2666 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2667 return Ok(self.attach_query_metadata(self.make_sql_df("use", empty), query));
2668 }
2669 if let Some(rewrite) = statement_completion::rewrite_show_databases(query) {
2670 let dataframe = self.context.sql(&rewrite).await?;
2671 return Ok(
2672 self.attach_query_metadata(self.make_sql_df("show-databases", dataframe), query)
2673 );
2674 }
2675
2676 if create_function_ddl::is_create_function_returns_table(query) {
2681 let ddl = create_function_ddl::parse_create_function(query)
2682 .map_err(|message| SqlError::InvalidTableFunction { message })?;
2683 if ddl.language.as_deref() != Some("sql") {
2684 return Err(SqlError::Unsupported {
2685 feature: format!(
2686 "CREATE FUNCTION '{}' uses language {:?}; only LANGUAGE SQL AS '...' \
2687 table functions are executable",
2688 ddl.function_name, ddl.language
2689 ),
2690 });
2691 }
2692 let body = ddl
2693 .body
2694 .as_deref()
2695 .filter(|body| !body.trim().is_empty())
2696 .ok_or_else(|| SqlError::InvalidTableFunction {
2697 message: format!(
2698 "SQL table function '{}' requires a non-empty AS body",
2699 ddl.function_name
2700 ),
2701 })?;
2702 let fields: Vec<_> = ddl
2703 .return_columns
2704 .iter()
2705 .map(|column| {
2706 arrow::datatypes::Field::new(&column.name, column.data_type.clone(), true)
2707 })
2708 .collect();
2709 let schema = arrow::datatypes::Schema::new(fields);
2710 let udf: std::sync::Arc<dyn krishiv_plan::udf::TableUdf> = std::sync::Arc::new(
2711 create_function_ddl::SqlBodyTableUdf::try_new(
2712 &ddl.function_name,
2713 schema,
2714 body,
2715 ddl.arguments.len(),
2716 std::sync::Arc::new(self.context.clone()),
2717 )
2718 .map_err(|error| SqlError::InvalidTableFunction {
2719 message: error.to_string(),
2720 })?,
2721 );
2722 if let Some(registry) = &self.udf_registry {
2723 let mut guard = registry.write().map_err(|e| SqlError::DataFusion {
2724 message: e.to_string(),
2725 })?;
2726 guard.register_table(std::sync::Arc::clone(&udf));
2727 }
2728 udf::register_single_table_udf(&self.context, std::sync::Arc::clone(&udf))
2729 .map_err(SqlError::from)?;
2730 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2731 return Ok(
2732 self.attach_query_metadata(self.make_sql_df("create-function", empty), query)
2733 );
2734 }
2735
2736 if query
2737 .trim_start()
2738 .to_ascii_uppercase()
2739 .starts_with("MERGE INTO")
2740 {
2741 let batches = lakehouse::execute_merge_sql(&self.context, query).await?;
2742 let merge_table = next_ephemeral_name("merge_result");
2743 lakehouse::register_scan_batches(&self.context, &merge_table, batches).await?;
2744 let dataframe = self
2745 .context
2746 .sql(&format!("SELECT * FROM {merge_table}"))
2747 .await?;
2748 return Ok(self.attach_query_metadata(self.make_sql_df("merge", dataframe), query));
2749 }
2750
2751 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2760 if trimmed.to_ascii_uppercase().starts_with("CREATE ")
2761 && let Some(parsed_ctas) = parse_ctas(trimmed)
2762 {
2763 let resolved = self.resolve_iceberg_table(&parsed_ctas.table_ref);
2764 if resolved.is_none() && !parsed_ctas.partition_by.is_empty() {
2767 return Err(SqlError::DataFusion {
2768 message: format!(
2769 "PARTITIONED BY requires an Iceberg catalog table; `{}` does not \
2770 resolve to a registered Iceberg catalog",
2771 parsed_ctas.table_ref
2772 ),
2773 });
2774 }
2775 if let Some((iceberg_catalog, table_ident)) = resolved {
2776 return self
2777 .execute_iceberg_ctas(iceberg_catalog, table_ident, parsed_ctas, query)
2778 .await;
2779 }
2780 }
2781
2782 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2785 if trimmed.to_ascii_uppercase().starts_with("CALL SYSTEM.") {
2786 let result = self.dispatch_call_system(trimmed).await?;
2787 let call_table = next_ephemeral_name("call_result");
2788 lakehouse::register_scan_batches(&self.context, &call_table, vec![result]).await?;
2789 let dataframe = self
2790 .context
2791 .sql(&format!("SELECT * FROM {call_table}"))
2792 .await?;
2793 return Ok(self.attach_query_metadata(self.make_sql_df("call", dataframe), query));
2794 }
2795
2796 if trimmed
2802 .get(..14)
2803 .is_some_and(|p| p.eq_ignore_ascii_case("ANALYZE TABLE "))
2804 {
2805 let result = self.dispatch_analyze_table(trimmed).await?;
2806 let res_table = next_ephemeral_name("analyze_result");
2807 lakehouse::register_scan_batches(&self.context, &res_table, vec![result]).await?;
2808 let dataframe = self
2809 .context
2810 .sql(&format!("SELECT * FROM {res_table}"))
2811 .await?;
2812 return Ok(self.attach_query_metadata(self.make_sql_df("analyze", dataframe), query));
2813 }
2814
2815 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2819 if trimmed.to_ascii_uppercase().starts_with("DELETE FROM ")
2820 && let Some((table_ref, predicate)) = parse_dml_delete(trimmed)
2821 && let Some((iceberg_catalog, table_ident)) = self.resolve_iceberg_table(&table_ref)
2822 {
2823 use arrow::array::{ArrayRef, Int64Array};
2824 use arrow::datatypes::{DataType, Field, Schema};
2825 let (deleted, _) = krishiv_connectors::lakehouse::dml::iceberg_delete_where(
2826 iceberg_catalog,
2827 &table_ident,
2828 &predicate,
2829 &self.context,
2830 )
2831 .await
2832 .map_err(|e| SqlError::DataFusion {
2833 message: e.to_string(),
2834 })?;
2835 self.adjust_table_row_count_stat(&table_ref, -(deleted as i64));
2837 let schema = Arc::new(Schema::new(vec![Field::new(
2838 "deleted_rows",
2839 DataType::Int64,
2840 false,
2841 )]));
2842 let array: ArrayRef = Arc::new(Int64Array::from(vec![deleted as i64]));
2843 let batch =
2844 RecordBatch::try_new(schema, vec![array]).map_err(|e| SqlError::DataFusion {
2845 message: e.to_string(),
2846 })?;
2847 let res_table = next_ephemeral_name("delete_result");
2848 lakehouse::register_scan_batches(&self.context, &res_table, vec![batch]).await?;
2849 let dataframe = self
2850 .context
2851 .sql(&format!("SELECT * FROM {res_table}"))
2852 .await?;
2853 return Ok(self.attach_query_metadata(self.make_sql_df("delete", dataframe), query));
2854 }
2855
2856 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2858 if trimmed.to_ascii_uppercase().starts_with("UPDATE ")
2859 && let Some(parsed) = parse_dml_update(trimmed)
2860 && let Some((iceberg_catalog, table_ident)) =
2861 self.resolve_iceberg_table(&parsed.table_ref)
2862 {
2863 use arrow::array::{ArrayRef, Int64Array};
2864 use arrow::datatypes::{DataType, Field, Schema};
2865 let borrowed: Vec<(&str, &str)> = parsed
2866 .assignments
2867 .iter()
2868 .map(|(c, e)| (c.as_str(), e.as_str()))
2869 .collect();
2870 let pred = parsed.predicate.as_deref();
2871 let (updated, _) = krishiv_connectors::lakehouse::dml::iceberg_update_where(
2872 iceberg_catalog,
2873 &table_ident,
2874 &borrowed,
2875 pred,
2876 &self.context,
2877 )
2878 .await
2879 .map_err(|e| SqlError::DataFusion {
2880 message: e.to_string(),
2881 })?;
2882 let schema = Arc::new(Schema::new(vec![Field::new(
2883 "updated_rows",
2884 DataType::Int64,
2885 false,
2886 )]));
2887 let array: ArrayRef = Arc::new(Int64Array::from(vec![updated as i64]));
2888 let batch =
2889 RecordBatch::try_new(schema, vec![array]).map_err(|e| SqlError::DataFusion {
2890 message: e.to_string(),
2891 })?;
2892 let res_table = next_ephemeral_name("update_result");
2893 lakehouse::register_scan_batches(&self.context, &res_table, vec![batch]).await?;
2894 let dataframe = self
2895 .context
2896 .sql(&format!("SELECT * FROM {res_table}"))
2897 .await?;
2898 return Ok(self.attach_query_metadata(self.make_sql_df("update", dataframe), query));
2899 }
2900
2901 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2911 if trimmed.to_ascii_uppercase().starts_with("INSERT ")
2912 && let Some(parsed) = parse_dml_insert(trimmed)
2913 && parsed.columns.is_empty()
2914 && let Some((iceberg_catalog, table_ident)) =
2915 self.resolve_iceberg_table(&parsed.table_ref)
2916 {
2917 use arrow::array::{ArrayRef, Int64Array};
2918 use arrow::datatypes::{DataType, Field, Schema};
2919 let source_df = self.context.sql(&parsed.inner_query).await?;
2920 let stream = source_df
2921 .execute_stream()
2922 .await
2923 .map_err(|e| SqlError::DataFusion {
2924 message: e.to_string(),
2925 })?;
2926 let report = krishiv_connectors::lakehouse::dml::iceberg_append_into(
2927 iceberg_catalog,
2928 &table_ident,
2929 stream,
2930 )
2931 .await
2932 .map_err(|e| SqlError::DataFusion {
2933 message: e.to_string(),
2934 })?;
2935 self.adjust_table_row_count_stat(&parsed.table_ref, report.rows as i64);
2937 let schema = Arc::new(Schema::new(vec![Field::new(
2938 "inserted_rows",
2939 DataType::Int64,
2940 false,
2941 )]));
2942 let array: ArrayRef = Arc::new(Int64Array::from(vec![report.rows as i64]));
2943 let batch =
2944 RecordBatch::try_new(schema, vec![array]).map_err(|e| SqlError::DataFusion {
2945 message: e.to_string(),
2946 })?;
2947 let res_table = next_ephemeral_name("insert_result");
2948 lakehouse::register_scan_batches(&self.context, &res_table, vec![batch]).await?;
2949 let dataframe = self
2950 .context
2951 .sql(&format!("SELECT * FROM {res_table}"))
2952 .await?;
2953 return Ok(self.attach_query_metadata(self.make_sql_df("insert", dataframe), query));
2954 }
2955
2956 if query.to_ascii_uppercase().contains(" MATCH_RECOGNIZE ")
2960 && let Some(stmt) = cep_sql::parse_match_recognize(query)?
2961 {
2962 let is_streaming = self.is_streaming_source(&stmt.source_table);
2963 let streaming_limit = streaming_match_recognize_limit_from_env();
2971 let source_sql = if is_streaming {
2972 format!(
2973 "SELECT * FROM {} LIMIT {}",
2974 stmt.source_table, streaming_limit
2975 )
2976 } else {
2977 format!("SELECT * FROM {}", stmt.source_table)
2978 };
2979 let source_df = self.context.sql(&source_sql).await?;
2980 let source_batches = source_df.collect().await?;
2981 if is_streaming {
2982 tracing::warn!(
2983 source = %stmt.source_table,
2984 limit = streaming_limit,
2985 collected_rows = source_batches.iter().map(|b| b.num_rows()).sum::<usize>(),
2986 "MATCH_RECOGNIZE executed against a streaming source under \
2987 bounded materialisation; results only cover the first {0} rows \
2988 of the source. Set KRISHIV_MATCH_RECOGNIZE_STREAMING_LIMIT to a \
2989 larger value if your executor has the memory budget.",
2990 streaming_limit
2991 );
2992 }
2993 let results = cep_sql::execute_match_recognize(stmt, &source_batches)?;
2994 let cep_table = next_ephemeral_name("cep_result");
2995 lakehouse::register_scan_batches(&self.context, &cep_table, results).await?;
2996 let dataframe = self
2997 .context
2998 .sql(&format!("SELECT * FROM {cep_table}"))
2999 .await?;
3000 return Ok(self.attach_query_metadata(self.make_sql_df("cep", dataframe), query));
3001 }
3002
3003 let query = &pivot_sql::rewrite_pivot_unpivot(query)?;
3006
3007 let query = &streaming_tvf::rewrite_window_tvfs(query);
3009
3010 let (rewritten, as_ofs) =
3011 lakehouse::preprocess_as_of_sql(query).unwrap_or_else(|_| (query.to_string(), vec![]));
3012 lakehouse::apply_as_of_refs(&self.context, &as_ofs).await?;
3013
3014 let can_cache = as_ofs.is_empty();
3021 let shuffle_override = self
3022 .shuffle_partitions
3023 .read()
3024 .map(|g| *g)
3025 .unwrap_or_else(|e| *e.into_inner());
3026 if can_cache {
3027 let cached_plan: Option<datafusion::logical_expr::LogicalPlan> = self
3029 .plan_cache
3030 .lock()
3031 .unwrap_or_else(|e| e.into_inner())
3032 .get(&rewritten)
3033 .cloned();
3034 if let Some(plan) = cached_plan {
3035 let dataframe = self.context.execute_logical_plan(plan).await?;
3036 return Ok(self.attach_query_metadata(
3037 self.make_sql_df("sql-query", dataframe)
3038 .with_shuffle_partitions(shuffle_override),
3039 &rewritten,
3040 ));
3041 }
3042 }
3043
3044 if let Some(location) = extract_create_external_table_location(&rewritten) {
3053 self.register_s3_object_store_for_warehouse(&location)
3054 .map_err(|message| SqlError::DataFusion { message })?;
3055 }
3056
3057 let dataframe = self.context.sql(&rewritten).await?;
3058
3059 if let Some(table_name) = extract_create_external_table_name(&rewritten)
3063 && !table_name.is_empty()
3064 && let Ok(provider) = self.context.table_provider(&table_name).await
3065 {
3066 let maybe_rows = provider
3067 .statistics()
3068 .and_then(|s| s.num_rows.get_value().copied());
3069 if let Some(n) = maybe_rows
3070 && let Ok(mut counts) = self.table_row_counts.write()
3071 {
3072 counts.entry(table_name).or_insert(n as u64);
3073 }
3074 }
3075
3076 if can_cache {
3078 let plan = dataframe.logical_plan().clone();
3079 match self.plan_cache.lock() {
3080 Ok(mut cache) => cache.insert(rewritten.clone(), plan),
3081 Err(poisoned) => poisoned.into_inner().insert(rewritten.clone(), plan),
3082 }
3083 }
3084
3085 Ok(self.attach_query_metadata(
3086 self.make_sql_df("sql-query", dataframe)
3087 .with_shuffle_partitions(shuffle_override),
3088 &rewritten,
3089 ))
3090 }
3091
3092 pub async fn execute_with_timeout(
3099 &self,
3100 query: impl AsRef<str> + Send,
3101 timeout_ms: u64,
3102 ) -> SqlResult<SqlDataFrame> {
3103 let timeout = std::time::Duration::from_millis(timeout_ms);
3104 tokio::time::timeout(timeout, self.sql(query))
3105 .await
3106 .map_err(|_| SqlError::Timeout { timeout_ms })?
3107 }
3108
3109 pub async fn execute_with_operation_id(
3116 &self,
3117 operation_id: u64,
3118 query: impl AsRef<str> + Send,
3119 cancelled_ids: &OperationRegistry,
3120 ) -> SqlResult<TaggedQueryResult> {
3121 if cancelled_ids.is_cancelled(operation_id) {
3122 return Err(SqlError::OperationCancelled { operation_id });
3123 }
3124 let df = self.sql(query).await?;
3125 Ok(TaggedQueryResult {
3126 operation_id,
3127 inner: df,
3128 })
3129 }
3130
3131 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
3137 fn resolve_iceberg_table(
3138 &self,
3139 table_ref: &str,
3140 ) -> Option<(Arc<dyn iceberg::Catalog + Send + Sync>, iceberg::TableIdent)> {
3141 let parts: Vec<&str> = table_ref.splitn(3, '.').collect();
3142 let (catalog_arc, ns_str, table_str) = {
3143 let guard = self
3144 .iceberg_catalogs
3145 .read()
3146 .unwrap_or_else(|e| e.into_inner());
3147 if guard.is_empty() {
3148 return None;
3149 }
3150 match parts.len() {
3151 2 => {
3152 let (cat, _) = guard.first()?;
3153 (Arc::clone(cat), *parts.first()?, *parts.get(1)?)
3154 }
3155 3 => {
3156 let cat_name = parts.first().copied()?;
3157 let (cat, _) = guard.iter().find(|(_, n)| n == cat_name)?;
3158 (Arc::clone(cat), *parts.get(1)?, *parts.get(2)?)
3159 }
3160 _ => return None,
3161 }
3162 };
3163 let ns = iceberg::NamespaceIdent::from_vec(vec![ns_str.to_string()]).ok()?;
3164 let ident = iceberg::TableIdent::new(ns, table_str.to_string());
3165 Some((catalog_arc.as_iceberg(), ident))
3166 }
3167
3168 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
3174 async fn execute_iceberg_ctas(
3175 &self,
3176 iceberg_catalog: Arc<dyn iceberg::Catalog + Send + Sync>,
3177 table_ident: iceberg::TableIdent,
3178 parsed_ctas: ParsedCtas,
3179 query: &str,
3180 ) -> SqlResult<SqlDataFrame> {
3181 use arrow::array::{ArrayRef, Int64Array};
3182 use arrow::datatypes::{DataType, Field, Schema};
3183 use krishiv_connectors::lakehouse::partitioned_write::parse_partition_transform;
3184
3185 let partition_by = parsed_ctas
3186 .partition_by
3187 .iter()
3188 .map(|item| parse_partition_transform(item))
3189 .collect::<Result<Vec<_>, _>>()
3190 .map_err(|e| SqlError::DataFusion {
3191 message: e.to_string(),
3192 })?;
3193
3194 let dataframe = self.context.sql(&parsed_ctas.inner_query).await?;
3195 let stream = dataframe
3196 .execute_stream()
3197 .await
3198 .map_err(|e| SqlError::DataFusion {
3199 message: e.to_string(),
3200 })?;
3201 let report = krishiv_connectors::lakehouse::dml::land_ctas(
3202 iceberg_catalog,
3203 &table_ident,
3204 parsed_ctas.or_replace,
3205 &partition_by,
3206 stream,
3207 )
3208 .await
3209 .map_err(|e| SqlError::DataFusion {
3210 message: e.to_string(),
3211 })?;
3212 self.invalidate_plan_cache();
3214 self.record_table_row_count_stat(&parsed_ctas.table_ref, report.rows as u64);
3216
3217 let schema = Arc::new(Schema::new(vec![
3218 Field::new("rows_written", DataType::Int64, false),
3219 Field::new("bytes_written", DataType::Int64, false),
3220 Field::new("data_files", DataType::Int64, false),
3221 Field::new("snapshot_id", DataType::Int64, false),
3222 ]));
3223 let columns: Vec<ArrayRef> = vec![
3224 Arc::new(Int64Array::from(vec![report.rows as i64])),
3225 Arc::new(Int64Array::from(vec![report.bytes as i64])),
3226 Arc::new(Int64Array::from(vec![report.data_files as i64])),
3227 Arc::new(Int64Array::from(vec![report.snapshot_id])),
3228 ];
3229 let batch = RecordBatch::try_new(schema, columns).map_err(|e| SqlError::DataFusion {
3230 message: e.to_string(),
3231 })?;
3232 let res_table = next_ephemeral_name("ctas_result");
3233 lakehouse::register_scan_batches(&self.context, &res_table, vec![batch]).await?;
3234 let dataframe = self
3235 .context
3236 .sql(&format!("SELECT * FROM {res_table}"))
3237 .await?;
3238 Ok(self.attach_query_metadata(self.make_sql_df("ctas", dataframe), query))
3239 }
3240
3241 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
3246 fn record_table_row_count_stat(&self, table_ref: &str, row_count: u64) {
3247 let registry = krishiv_plan::optimizer::global_table_stats();
3248 let mut names = vec![table_ref];
3249 let bare = table_ref.rsplit('.').next().unwrap_or(table_ref);
3250 if bare != table_ref {
3251 names.push(bare);
3252 }
3253 for name in &names {
3254 let mut stats = registry
3255 .get(name)
3256 .unwrap_or_else(|| krishiv_plan::optimizer::TableCboStats::new(*name));
3257 stats.row_count = Some(row_count);
3258 registry.put(stats);
3259 }
3260 if let Ok(mut counts) = self.table_row_counts.write() {
3261 for name in &names {
3262 counts.insert((*name).to_owned(), row_count);
3263 }
3264 }
3265 }
3266
3267 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
3272 fn adjust_table_row_count_stat(&self, table_ref: &str, delta: i64) {
3273 let registry = krishiv_plan::optimizer::global_table_stats();
3274 let mut names = vec![table_ref];
3275 let bare = table_ref.rsplit('.').next().unwrap_or(table_ref);
3276 if bare != table_ref {
3277 names.push(bare);
3278 }
3279 for name in &names {
3280 if let Some(mut stats) = registry.get(name)
3281 && let Some(current) = stats.row_count
3282 {
3283 stats.row_count = Some(current.saturating_add_signed(delta));
3284 registry.put(stats);
3285 }
3286 }
3287 if let Ok(mut counts) = self.table_row_counts.write() {
3288 for name in &names {
3289 if let Some(current) = counts.get(*name).copied() {
3290 counts.insert((*name).to_owned(), current.saturating_add_signed(delta));
3291 }
3292 }
3293 }
3294 }
3295
3296 async fn dispatch_analyze_table(&self, stmt: &str) -> SqlResult<RecordBatch> {
3307 use arrow::array::{ArrayRef, Int64Array, StringArray};
3308 use arrow::datatypes::{DataType, Field, Schema};
3309
3310 let rest = stmt
3311 .get(14..)
3312 .unwrap_or("")
3313 .trim()
3314 .trim_end_matches(';')
3315 .trim();
3316 let (table_ref, tail) = match rest.split_once(char::is_whitespace) {
3317 Some((t, tail)) => (t.trim(), tail.trim()),
3318 None => (rest, ""),
3319 };
3320 if table_ref.is_empty() {
3321 return Err(SqlError::DataFusion {
3322 message: String::from("ANALYZE TABLE: table reference is required"),
3323 });
3324 }
3325 let mut tail = tail;
3327 if tail
3328 .get(..18)
3329 .is_some_and(|p| p.eq_ignore_ascii_case("COMPUTE STATISTICS"))
3330 {
3331 tail = tail.get(18..).unwrap_or("").trim();
3332 }
3333 let columns: Vec<String> = if tail
3334 .get(..11)
3335 .is_some_and(|p| p.eq_ignore_ascii_case("FOR COLUMNS"))
3336 {
3337 tail.get(11..)
3338 .unwrap_or("")
3339 .trim()
3340 .trim_start_matches('(')
3341 .trim_end_matches(')')
3342 .split(',')
3343 .map(|c| c.trim().trim_matches('"').to_owned())
3344 .filter(|c| !c.is_empty())
3345 .collect()
3346 } else if tail.is_empty() {
3347 Vec::new()
3348 } else {
3349 return Err(SqlError::DataFusion {
3350 message: format!("ANALYZE TABLE: unexpected trailing clause: {tail}"),
3351 });
3352 };
3353
3354 let mut projections = vec![String::from("count(*)")];
3356 for c in &columns {
3357 projections.push(format!("approx_distinct(\"{c}\")"));
3358 projections.push(format!("min(\"{c}\")"));
3359 projections.push(format!("max(\"{c}\")"));
3360 projections.push(format!("count(\"{c}\")"));
3361 }
3362 let scan_sql = format!("SELECT {} FROM {table_ref}", projections.join(", "));
3363 let batches = self.context.sql(&scan_sql).await?.collect().await?;
3364 let row =
3365 batches
3366 .iter()
3367 .find(|b| b.num_rows() > 0)
3368 .ok_or_else(|| SqlError::DataFusion {
3369 message: format!("ANALYZE TABLE {table_ref}: aggregation returned no rows"),
3370 })?;
3371 let cell_string = |col: usize| -> Option<String> {
3372 let column = row.columns().get(col)?;
3373 if column.is_null(0) {
3374 return None;
3375 }
3376 arrow::util::display::array_value_to_string(column, 0).ok()
3377 };
3378 let cell_u64 = |col: usize| -> Option<u64> { cell_string(col)?.parse().ok() };
3379 let row_count = cell_u64(0).ok_or_else(|| SqlError::DataFusion {
3380 message: format!("ANALYZE TABLE {table_ref}: COUNT(*) unreadable"),
3381 })?;
3382
3383 let mut column_stats = Vec::with_capacity(columns.len());
3384 for (i, name) in columns.iter().enumerate() {
3385 let base = 1 + i * 4;
3386 let non_null = cell_u64(base + 3);
3387 column_stats.push(krishiv_plan::optimizer::ColumnCboStats {
3388 name: name.clone(),
3389 ndv: cell_u64(base),
3390 min: cell_string(base + 1),
3391 max: cell_string(base + 2),
3392 null_count: non_null.map(|n| row_count.saturating_sub(n)),
3393 });
3394 }
3395
3396 let avg_row_bytes = match self.context.table_provider(table_ref).await {
3398 Ok(provider) => provider.statistics().and_then(|s| {
3399 let rows = s.num_rows.get_value().copied()?;
3400 let bytes = s.total_byte_size.get_value().copied()?;
3401 (rows > 0).then(|| (bytes / rows) as u64)
3402 }),
3403 Err(_) => None,
3404 };
3405
3406 let mut stats =
3407 krishiv_plan::optimizer::TableCboStats::new(table_ref).with_row_count(row_count);
3408 if let Some(bytes) = avg_row_bytes {
3409 stats = stats.with_avg_row_bytes(bytes);
3410 }
3411 if let Some(max_ndv) = column_stats.iter().filter_map(|c| c.ndv).max() {
3412 stats = stats.with_ndv(max_ndv);
3414 }
3415 stats.columns = column_stats;
3416 let registry = krishiv_plan::optimizer::global_table_stats();
3417 let bare = table_ref.rsplit('.').next().unwrap_or(table_ref);
3420 if bare != table_ref {
3421 let mut bare_stats = stats.clone();
3422 bare_stats.table = bare.to_owned();
3423 registry.put(bare_stats);
3424 }
3425 let analyzed_columns = stats.columns.len();
3426 registry.put(stats);
3427 if let Ok(mut counts) = self.table_row_counts.write() {
3428 counts.insert(table_ref.to_owned(), row_count);
3429 if bare != table_ref {
3430 counts.insert(bare.to_owned(), row_count);
3431 }
3432 }
3433 self.invalidate_plan_cache();
3434
3435 let schema = Arc::new(Schema::new(vec![
3436 Field::new("table_name", DataType::Utf8, false),
3437 Field::new("row_count", DataType::Int64, false),
3438 Field::new("avg_row_bytes", DataType::Int64, true),
3439 Field::new("columns_analyzed", DataType::Int64, false),
3440 ]));
3441 let columns_out: Vec<ArrayRef> = vec![
3442 Arc::new(StringArray::from(vec![table_ref.to_owned()])),
3443 Arc::new(Int64Array::from(vec![row_count as i64])),
3444 Arc::new(Int64Array::from(vec![avg_row_bytes.map(|b| b as i64)])),
3445 Arc::new(Int64Array::from(vec![analyzed_columns as i64])),
3446 ];
3447 RecordBatch::try_new(schema, columns_out).map_err(|e| SqlError::DataFusion {
3448 message: e.to_string(),
3449 })
3450 }
3451
3452 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
3455 async fn dispatch_call_system(&self, stmt: &str) -> SqlResult<RecordBatch> {
3456 use arrow::array::{ArrayRef, Int64Array};
3457 use arrow::datatypes::{DataType, Field, Schema};
3458
3459 let upper = stmt.to_ascii_uppercase();
3460 const PREFIX: &str = "CALL SYSTEM.";
3461 let upper_after = &upper[PREFIX.len()..];
3462 let orig_after = &stmt[PREFIX.len()..];
3463
3464 let paren = upper_after.find('(').ok_or_else(|| SqlError::DataFusion {
3465 message: format!("CALL: missing '(' in: {stmt}"),
3466 })?;
3467 let proc_name = upper_after[..paren].trim();
3468
3469 let args_raw = orig_after[paren + 1..]
3470 .trim_end_matches(';')
3471 .trim()
3472 .trim_end_matches(')')
3473 .trim();
3474 let args = call_args_from_str(args_raw);
3475
3476 let iceberg_catalog = {
3477 let guard = self
3478 .iceberg_catalogs
3479 .read()
3480 .unwrap_or_else(|e| e.into_inner());
3481 guard
3482 .first()
3483 .ok_or_else(|| SqlError::DataFusion {
3484 message: "CALL system: no Iceberg catalog registered".to_string(),
3485 })?
3486 .0
3487 .as_iceberg()
3488 };
3489
3490 let table_ref = args.first().ok_or_else(|| SqlError::DataFusion {
3491 message: format!("CALL {proc_name}: table reference argument is required"),
3492 })?;
3493 let table_ident = iceberg_table_ident(table_ref)?;
3494
3495 if proc_name == "MAINTAIN_TABLE" {
3499 let older_than = parse_call_duration(args.get(1).map_or("7 days", |s| s.as_str()))?;
3500 let target_bytes = args
3501 .get(2)
3502 .and_then(|s| s.parse::<u64>().ok())
3503 .unwrap_or(128 * 1024 * 1024);
3504 let retain_last = args
3505 .get(3)
3506 .and_then(|s| s.parse::<usize>().ok())
3507 .unwrap_or(1);
3508 let report = krishiv_connectors::lakehouse::maintenance::maintain_table(
3509 iceberg_catalog,
3510 &table_ident,
3511 target_bytes,
3512 older_than,
3513 retain_last,
3514 )
3515 .await
3516 .map_err(|e| SqlError::DataFusion {
3517 message: e.to_string(),
3518 })?;
3519 let schema = Arc::new(Schema::new(vec![
3520 Field::new("compacted_files", DataType::Int64, false),
3521 Field::new("expired_snapshots", DataType::Int64, false),
3522 Field::new("removed_orphans", DataType::Int64, false),
3523 ]));
3524 let columns: Vec<ArrayRef> = vec![
3525 Arc::new(Int64Array::from(vec![report.compacted_files as i64])),
3526 Arc::new(Int64Array::from(vec![report.expired_snapshots as i64])),
3527 Arc::new(Int64Array::from(vec![report.removed_orphans as i64])),
3528 ];
3529 return RecordBatch::try_new(schema, columns).map_err(|e| SqlError::DataFusion {
3530 message: e.to_string(),
3531 });
3532 }
3533
3534 let count: i64 = match proc_name {
3535 "EXPIRE_SNAPSHOTS" => {
3536 let dur_s = args.get(1).ok_or_else(|| SqlError::DataFusion {
3537 message: "CALL expire_snapshots: duration argument is required".to_string(),
3538 })?;
3539 let older_than = parse_call_duration(dur_s)?;
3540 let retain_last = args
3541 .get(2)
3542 .and_then(|s| s.parse::<usize>().ok())
3543 .unwrap_or(1);
3544 krishiv_connectors::lakehouse::maintenance::expire_snapshots(
3545 iceberg_catalog,
3546 &table_ident,
3547 older_than,
3548 retain_last,
3549 )
3550 .await
3551 .map_err(|e| SqlError::DataFusion {
3552 message: e.to_string(),
3553 })? as i64
3554 }
3555 "REMOVE_ORPHAN_FILES" => {
3556 let dur_s = args.get(1).ok_or_else(|| SqlError::DataFusion {
3557 message: "CALL remove_orphan_files: duration argument is required".to_string(),
3558 })?;
3559 let older_than = parse_call_duration(dur_s)?;
3560 krishiv_connectors::lakehouse::maintenance::remove_orphan_files(
3561 iceberg_catalog,
3562 &table_ident,
3563 older_than,
3564 )
3565 .await
3566 .map_err(|e| SqlError::DataFusion {
3567 message: e.to_string(),
3568 })? as i64
3569 }
3570 "COMPACT_DATA_FILES" => {
3571 let target_bytes = args
3572 .get(1)
3573 .and_then(|s| s.parse::<u64>().ok())
3574 .unwrap_or(128 * 1024 * 1024);
3575 krishiv_connectors::lakehouse::maintenance::compact_data_files(
3576 iceberg_catalog,
3577 &table_ident,
3578 target_bytes,
3579 )
3580 .await
3581 .map_err(|e| SqlError::DataFusion {
3582 message: e.to_string(),
3583 })? as i64
3584 }
3585 other => {
3586 return Err(SqlError::Unsupported {
3587 feature: format!("CALL system.{other}: unknown procedure"),
3588 });
3589 }
3590 };
3591
3592 let col = match proc_name {
3593 "EXPIRE_SNAPSHOTS" => "expired_snapshots",
3594 "REMOVE_ORPHAN_FILES" => "removed_files",
3595 "COMPACT_DATA_FILES" => "rewritten_files",
3596 _ => "result",
3597 };
3598 let schema = Arc::new(Schema::new(vec![Field::new(col, DataType::Int64, false)]));
3599 let array: ArrayRef = Arc::new(Int64Array::from(vec![count]));
3600 RecordBatch::try_new(schema, vec![array]).map_err(|e| SqlError::DataFusion {
3601 message: e.to_string(),
3602 })
3603 }
3604}
3605
3606pub struct TaggedQueryResult {
3608 pub operation_id: u64,
3610 pub inner: SqlDataFrame,
3612}
3613
3614#[derive(Clone, Default)]
3620pub struct OperationRegistry {
3621 cancelled: Arc<std::sync::RwLock<std::collections::HashSet<u64>>>,
3622 progress: Arc<std::sync::RwLock<std::collections::HashMap<u64, (u64, u64)>>>,
3623}
3624
3625impl OperationRegistry {
3626 pub fn new() -> Self {
3628 Self::default()
3629 }
3630
3631 pub fn cancel(&self, operation_id: u64) {
3635 if let Ok(mut ids) = self.cancelled.write() {
3636 ids.insert(operation_id);
3637 }
3638 }
3639
3640 pub fn is_cancelled(&self, operation_id: u64) -> bool {
3642 self.cancelled
3643 .read()
3644 .map(|ids| ids.contains(&operation_id))
3645 .unwrap_or(false)
3646 }
3647
3648 pub fn remove(&self, operation_id: u64) {
3650 if let Ok(mut ids) = self.cancelled.write() {
3651 ids.remove(&operation_id);
3652 }
3653 if let Ok(mut progress) = self.progress.write() {
3654 progress.remove(&operation_id);
3655 }
3656 }
3657
3658 pub fn update_progress(&self, operation_id: u64, rows_scanned: u64, rows_emitted: u64) {
3660 if let Ok(mut progress) = self.progress.write() {
3661 progress.insert(operation_id, (rows_scanned, rows_emitted));
3662 }
3663 }
3664
3665 pub fn progress(&self, operation_id: u64) -> Option<(u64, u64)> {
3667 self.progress
3668 .read()
3669 .ok()
3670 .and_then(|progress| progress.get(&operation_id).copied())
3671 }
3672
3673 pub fn cancelled_ids(&self) -> Vec<u64> {
3675 self.cancelled
3676 .read()
3677 .map(|ids| ids.iter().copied().collect())
3678 .unwrap_or_default()
3679 }
3680}
3681
3682pub(crate) fn extract_create_external_table_name(query: &str) -> Option<String> {
3687 use datafusion::sql::parser::{DFParser, Statement as DFStatement};
3688 let mut stmts = DFParser::parse_sql(query).ok()?;
3689 match stmts.pop_front()? {
3690 DFStatement::CreateExternalTable(create) => Some(create.name.to_string()),
3691 _ => None,
3692 }
3693}
3694
3695pub(crate) fn extract_create_external_table_location(query: &str) -> Option<String> {
3703 use datafusion::sql::parser::{DFParser, Statement as DFStatement};
3704 let mut stmts = DFParser::parse_sql(query).ok()?;
3705 match stmts.pop_front()? {
3706 DFStatement::CreateExternalTable(create) => Some(create.location),
3707 _ => None,
3708 }
3709}
3710
3711pub enum GroupingMode<'a> {
3719 Sets(Vec<Vec<&'a krishiv_plan::expression::Expr>>),
3720 Cube(Vec<&'a krishiv_plan::expression::Expr>),
3721 Rollup(Vec<&'a krishiv_plan::expression::Expr>),
3722}
3723
3724#[async_trait::async_trait]
3725pub trait KrishivDataFrameOps: Send + Sync {
3726 async fn collect(&self) -> SqlResult<Vec<RecordBatch>>;
3728 async fn collect_with_stats(&self) -> SqlResult<(Vec<RecordBatch>, SqlExecutionStats)>;
3730 async fn explain(&self) -> SqlResult<String>;
3732
3733 async fn explain_analyze(&self) -> SqlResult<String> {
3739 Err(SqlError::DataFusion {
3740 message: String::from("EXPLAIN ANALYZE is not supported for this dataframe backend"),
3741 })
3742 }
3743 fn explain_logical(&self) -> String;
3745 fn krishiv_logical_plan(&self) -> LogicalPlan;
3747 fn query(&self) -> Option<&str>;
3749 fn to_sql(&self) -> SqlResult<String> {
3753 Err(SqlError::Unsupported {
3754 feature: "to_sql (plan unparsing) is not supported for this DataFrame".into(),
3755 })
3756 }
3757 async fn execute_stream(&self) -> SqlResult<SqlStream>;
3759
3760 fn schema(&self) -> SchemaRef;
3764
3765 async fn select(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3767
3768 async fn select_exprs(
3770 &self,
3771 expressions: &[&krishiv_plan::expression::Expr],
3772 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3773
3774 async fn unnest_columns(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3778
3779 async fn aggregate(
3781 &self,
3782 group_exprs: &[&krishiv_plan::expression::Expr],
3783 aggregate_exprs: &[&krishiv_plan::expression::Expr],
3784 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3785
3786 async fn aggregate_grouping(
3788 &self,
3789 grouping: GroupingMode<'_>,
3790 aggregate_exprs: &[&krishiv_plan::expression::Expr],
3791 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3792
3793 async fn pivot(
3795 &self,
3796 group_exprs: &[&krishiv_plan::expression::Expr],
3797 pivot_column: &krishiv_plan::expression::Expr,
3798 aggregate_expr: &krishiv_plan::expression::Expr,
3799 values: &[(krishiv_plan::expression::ScalarValue, String)],
3800 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3801
3802 async fn unpivot(
3804 &self,
3805 columns: &[&str],
3806 name_column: &str,
3807 value_column: &str,
3808 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3809
3810 async fn filter(&self, predicate: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3812
3813 async fn filter_expr(
3815 &self,
3816 predicate: &krishiv_plan::expression::Expr,
3817 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3818
3819 async fn limit(&self, n: usize) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3821
3822 async fn distinct(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3824
3825 async fn drop_nulls(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3827
3828 async fn sample(&self, fraction: f64) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3830
3831 async fn sort(
3833 &self,
3834 columns: &[&str],
3835 descending: &[bool],
3836 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3837
3838 async fn alias(&self, alias: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3840
3841 async fn drop_columns(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3843
3844 async fn rename_column(&self, old: &str, new: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3846
3847 async fn with_column(&self, name: &str, expr: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3849
3850 fn as_any(&self) -> &dyn std::any::Any;
3852
3853 async fn describe(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3855
3856 async fn fill_null(&self, column: &str, value: &str)
3858 -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3859
3860 async fn join(
3862 &self,
3863 right: &dyn KrishivDataFrameOps,
3864 how: &str,
3865 left_on: &[&str],
3866 right_on: &[&str],
3867 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3868
3869 async fn union(
3871 &self,
3872 right: &dyn KrishivDataFrameOps,
3873 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3874
3875 async fn union_distinct(
3876 &self,
3877 right: &dyn KrishivDataFrameOps,
3878 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3879
3880 async fn intersect(
3881 &self,
3882 right: &dyn KrishivDataFrameOps,
3883 distinct: bool,
3884 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3885
3886 async fn except(
3887 &self,
3888 right: &dyn KrishivDataFrameOps,
3889 distinct: bool,
3890 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3891
3892 async fn register_batches(&self, name: &str, batches: Vec<RecordBatch>) -> SqlResult<()>;
3895
3896 async fn deregister_table(&self, name: &str) -> SqlResult<()>;
3898
3899 async fn create_view(&self, name: &str, replace: bool) -> SqlResult<()>;
3902}
3903
3904fn df_plan_to_krishiv_nodes(
3912 plan: &datafusion::logical_expr::LogicalPlan,
3913 table_row_counts: &std::collections::HashMap<String, u64>,
3914 counter: &mut usize,
3915) -> (Vec<krishiv_plan::PlanNode>, String) {
3916 use datafusion::logical_expr::LogicalPlan as DfPlan;
3917 use krishiv_plan::{ExecutionKind, NodeOp, PlanNode};
3918
3919 *counter += 1;
3920 let idx = *counter;
3921
3922 match plan {
3923 DfPlan::TableScan(ts) => {
3924 let table_name = ts.table_name.table().to_string();
3925 let row_count = table_row_counts.get(&table_name).copied();
3926 let filters: Vec<String> = ts.filters.iter().map(|e| e.to_string()).collect();
3927 let id = format!("scan-{idx}");
3928 let node = PlanNode::new(&id, format!("Scan {table_name}"), ExecutionKind::Batch)
3929 .with_op(NodeOp::Scan {
3930 table: table_name,
3931 filters,
3932 })
3933 .with_estimated_rows(row_count);
3934 (vec![node], id)
3935 }
3936
3937 DfPlan::Projection(proj) => {
3938 let (mut nodes, input_id) =
3939 df_plan_to_krishiv_nodes(&proj.input, table_row_counts, counter);
3940 let id = format!("proj-{idx}");
3941 let columns: Vec<String> = proj.expr.iter().map(|e| e.to_string()).collect();
3942 nodes.push(
3943 PlanNode::new(&id, "Projection", ExecutionKind::Batch)
3944 .with_op(NodeOp::Project { columns })
3945 .with_inputs([input_id]),
3946 );
3947 (nodes, id)
3948 }
3949
3950 DfPlan::Filter(filter) => {
3951 let (mut nodes, input_id) =
3952 df_plan_to_krishiv_nodes(&filter.input, table_row_counts, counter);
3953 let id = format!("filter-{idx}");
3954 let predicate = filter.predicate.to_string();
3955 nodes.push(
3956 PlanNode::new(&id, "Filter", ExecutionKind::Batch)
3957 .with_op(NodeOp::Filter { predicate })
3958 .with_inputs([input_id]),
3959 );
3960 (nodes, id)
3961 }
3962
3963 DfPlan::Aggregate(agg) => {
3964 let (mut nodes, input_id) =
3965 df_plan_to_krishiv_nodes(&agg.input, table_row_counts, counter);
3966 let id = format!("agg-{idx}");
3967 let group_keys: Vec<String> = agg.group_expr.iter().map(|e| e.to_string()).collect();
3968 nodes.push(
3969 PlanNode::new(&id, "Aggregate", ExecutionKind::Batch)
3970 .with_op(NodeOp::Aggregate { group_keys })
3971 .with_inputs([input_id]),
3972 );
3973 (nodes, id)
3974 }
3975
3976 DfPlan::Join(join) => {
3977 let (mut nodes, left_id) =
3978 df_plan_to_krishiv_nodes(&join.left, table_row_counts, counter);
3979 let (right_nodes, right_id) =
3980 df_plan_to_krishiv_nodes(&join.right, table_row_counts, counter);
3981 nodes.extend(right_nodes);
3982 let id = format!("join-{idx}");
3983 let krishiv_join_type = match join.join_type {
3988 datafusion::common::JoinType::Inner => krishiv_plan::JoinType::Inner,
3989 datafusion::common::JoinType::Left => krishiv_plan::JoinType::Left,
3990 datafusion::common::JoinType::Right => krishiv_plan::JoinType::Right,
3991 datafusion::common::JoinType::Full => krishiv_plan::JoinType::Full,
3992 datafusion::common::JoinType::LeftSemi => krishiv_plan::JoinType::LeftSemi,
3993 datafusion::common::JoinType::RightSemi => krishiv_plan::JoinType::RightSemi,
3994 datafusion::common::JoinType::LeftAnti => krishiv_plan::JoinType::LeftAnti,
3995 datafusion::common::JoinType::RightAnti => krishiv_plan::JoinType::RightAnti,
3996 datafusion::common::JoinType::LeftMark => krishiv_plan::JoinType::LeftSemi,
4000 datafusion::common::JoinType::RightMark => krishiv_plan::JoinType::RightSemi,
4001 };
4002 nodes.push(
4003 PlanNode::new(&id, "Join", ExecutionKind::Batch)
4004 .with_op(NodeOp::Join {
4005 join_type: krishiv_join_type,
4006 })
4007 .with_inputs([left_id, right_id]),
4008 );
4009 (nodes, id)
4010 }
4011
4012 DfPlan::Sort(sort) => {
4013 let (mut nodes, input_id) =
4014 df_plan_to_krishiv_nodes(&sort.input, table_row_counts, counter);
4015 let id = format!("sort-{idx}");
4016 nodes.push(
4017 PlanNode::new(&id, "Sort", ExecutionKind::Batch)
4018 .with_op(NodeOp::Other {
4019 description: format!(
4020 "Sort({})",
4021 sort.expr
4022 .iter()
4023 .map(|e| e.to_string())
4024 .collect::<Vec<_>>()
4025 .join(", ")
4026 ),
4027 })
4028 .with_inputs([input_id]),
4029 );
4030 (nodes, id)
4031 }
4032
4033 DfPlan::Repartition(repart) => {
4034 let (mut nodes, input_id) =
4035 df_plan_to_krishiv_nodes(&repart.input, table_row_counts, counter);
4036 let id = format!("exchange-{idx}");
4037 let partitioning = krishiv_plan::Partitioning::Unpartitioned;
4038 nodes.push(
4039 PlanNode::new(&id, "Exchange", ExecutionKind::Batch)
4040 .with_op(NodeOp::Exchange { partitioning })
4041 .with_inputs([input_id]),
4042 );
4043 (nodes, id)
4044 }
4045
4046 DfPlan::Limit(limit) => {
4047 let (mut nodes, input_id) =
4048 df_plan_to_krishiv_nodes(&limit.input, table_row_counts, counter);
4049 let id = format!("limit-{idx}");
4050 nodes.push(
4051 PlanNode::new(&id, "Limit", ExecutionKind::Batch)
4052 .with_op(NodeOp::Other {
4053 description: format!(
4054 "Limit(skip={:?}, fetch={:?})",
4055 limit.skip.as_ref().map(|e| e.to_string()),
4056 limit.fetch.as_ref().map(|e| e.to_string()),
4057 ),
4058 })
4059 .with_inputs([input_id]),
4060 );
4061 (nodes, id)
4062 }
4063
4064 DfPlan::Union(union) if union.inputs.len() == 1 => {
4065 if let Some(input) = union.inputs.first() {
4066 df_plan_to_krishiv_nodes(input, table_row_counts, counter)
4067 } else {
4068 (Vec::new(), String::new())
4069 }
4070 }
4071 DfPlan::Union(union) => {
4072 let mut all_nodes = Vec::new();
4073 let mut input_ids = Vec::new();
4074 for input in &union.inputs {
4075 let (sub_nodes, sub_id) =
4076 df_plan_to_krishiv_nodes(input, table_row_counts, counter);
4077 all_nodes.extend(sub_nodes);
4078 input_ids.push(sub_id);
4079 }
4080 let id = format!("union-{idx}");
4081 all_nodes.push(
4082 PlanNode::new(&id, "Union", ExecutionKind::Batch)
4083 .with_op(NodeOp::Other {
4084 description: "Union".to_string(),
4085 })
4086 .with_inputs(input_ids),
4087 );
4088 (all_nodes, id)
4089 }
4090
4091 DfPlan::SubqueryAlias(alias) => {
4092 df_plan_to_krishiv_nodes(&alias.input, table_row_counts, counter)
4094 }
4095
4096 DfPlan::Values(_) => {
4097 let id = format!("values-{idx}");
4098 let node = PlanNode::new(&id, "Values", ExecutionKind::Batch).with_op(NodeOp::Other {
4099 description: "Values".to_string(),
4100 });
4101 (vec![node], id)
4102 }
4103
4104 DfPlan::Extension(_) => {
4105 let id = format!("ext-{idx}");
4106 let label = plan.to_string();
4107 let node = PlanNode::new(&id, label.clone(), ExecutionKind::Batch)
4108 .with_op(NodeOp::Other { description: label });
4109 (vec![node], id)
4110 }
4111
4112 DfPlan::EmptyRelation(_) => {
4113 let id = format!("empty-{idx}");
4114 let node =
4115 PlanNode::new(&id, "EmptyRelation", ExecutionKind::Batch).with_op(NodeOp::Other {
4116 description: "EmptyRelation".to_string(),
4117 });
4118 (vec![node], id)
4119 }
4120
4121 _ => {
4123 let id = format!("df-{idx}");
4124 let label = plan.to_string();
4125 let node = PlanNode::new(&id, label.clone(), ExecutionKind::Batch)
4126 .with_op(NodeOp::Other { description: label });
4127 (vec![node], id)
4128 }
4129 }
4130}
4131
4132#[derive(Clone)]
4134pub struct SqlDataFrame {
4135 name: String,
4136 query: Option<String>,
4137 query_text: Option<String>,
4139 execution_kind: ExecutionKind,
4140 dataframe: DataFusionDataFrame,
4141 shuffle_partitions: Option<u32>,
4142 context: SessionContext,
4144 table_row_counts: Arc<std::sync::RwLock<HashMap<String, u64>>>,
4148}
4149
4150impl fmt::Debug for SqlDataFrame {
4151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4152 f.debug_struct("SqlDataFrame")
4153 .field("name", &self.name)
4154 .field("query", &self.query)
4155 .field("shuffle_partitions", &self.shuffle_partitions)
4156 .finish_non_exhaustive()
4157 }
4158}
4159
4160impl SqlDataFrame {
4161 fn new(
4162 name: impl Into<String>,
4163 dataframe: DataFusionDataFrame,
4164 table_row_counts: Arc<std::sync::RwLock<HashMap<String, u64>>>,
4165 ) -> Self {
4166 Self {
4167 name: name.into(),
4168 query: None,
4169 query_text: None,
4170 execution_kind: ExecutionKind::Batch,
4171 dataframe,
4172 shuffle_partitions: None,
4173 context: SessionContext::default(),
4174 table_row_counts,
4175 }
4176 }
4177
4178 pub(crate) fn with_context(mut self, context: SessionContext) -> Self {
4180 self.context = context;
4181 self
4182 }
4183
4184 fn with_query(mut self, query: impl Into<String>) -> Self {
4185 let q = query.into();
4186 self.query_text = Some(q.clone());
4187 self.query = Some(q);
4188 self
4189 }
4190
4191 fn with_execution_kind(mut self, kind: ExecutionKind) -> Self {
4192 self.execution_kind = kind;
4193 self
4194 }
4195
4196 fn with_shuffle_partitions(mut self, n: Option<u32>) -> Self {
4197 self.shuffle_partitions = n;
4198 self
4199 }
4200
4201 pub fn query(&self) -> Option<&str> {
4203 self.query.as_deref()
4204 }
4205
4206 pub fn arrow_schema(&self) -> arrow::datatypes::SchemaRef {
4212 std::sync::Arc::new(self.dataframe.schema().as_arrow().clone())
4213 }
4214
4215 fn with_new_dataframe(&self, df: DataFusionDataFrame, tag: &str) -> Self {
4219 Self {
4220 name: format!("{}-{}", self.name, tag),
4221 query: None,
4222 query_text: None,
4223 execution_kind: self.execution_kind,
4224 dataframe: df,
4225 shuffle_partitions: self.shuffle_partitions,
4226 context: self.context.clone(),
4227 table_row_counts: self.table_row_counts.clone(),
4228 }
4229 }
4230
4231 pub fn krishiv_logical_plan(&self) -> LogicalPlan {
4240 let df_plan = self.dataframe.logical_plan();
4241 let counts = self
4242 .table_row_counts
4243 .read()
4244 .unwrap_or_else(|e| e.into_inner());
4245 let mut counter = 0usize;
4246 let (nodes, _root_id) = df_plan_to_krishiv_nodes(df_plan, &counts, &mut counter);
4247
4248 let mut plan = LogicalPlan::new(self.name.clone(), self.execution_kind);
4249 for node in nodes {
4250 plan = plan.with_node(node);
4251 }
4252
4253 let optimizer = krishiv_plan::optimizer::default_logical_optimizer();
4258 let fallback = plan.clone();
4259 match optimizer.optimize(plan) {
4260 Ok(result) => result.plan,
4261 Err(error) => {
4262 tracing::warn!(
4263 plan = %self.name,
4264 %error,
4265 "logical optimizer failed; using unoptimized plan"
4266 );
4267 fallback
4268 }
4269 }
4270 }
4271
4272 pub fn explain_logical(&self) -> String {
4274 self.dataframe.logical_plan().to_string()
4275 }
4276
4277 pub async fn explain(&self) -> SqlResult<String> {
4279 let batches = self
4280 .dataframe
4281 .clone()
4282 .explain(false, false)?
4283 .collect()
4284 .await?;
4285 pretty_batches(&batches)
4286 }
4287
4288 pub async fn explain_analyze(&self) -> SqlResult<String> {
4303 let batches = self
4304 .dataframe
4305 .clone()
4306 .explain(false, true)?
4307 .collect()
4308 .await?;
4309 pretty_batches(&batches)
4310 }
4311
4312 pub fn collect(
4317 &self,
4318 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = SqlResult<Vec<RecordBatch>>> + Send + '_>>
4319 {
4320 Box::pin(async move { Ok(self.dataframe.clone().collect().await?) })
4321 }
4322
4323 pub fn execute_stream(
4329 &self,
4330 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = SqlResult<SqlStream>> + Send + '_>>
4331 {
4332 Box::pin(self.execute_stream_boxed_body())
4333 }
4334
4335 async fn execute_stream_boxed_body(&self) -> SqlResult<SqlStream> {
4336 Ok(self.execute_stream_with_schema_boxed_body().await?.1)
4337 }
4338
4339 pub fn execute_stream_with_schema(
4354 &self,
4355 ) -> futures::future::BoxFuture<'_, SqlResult<(SchemaRef, SqlStream)>> {
4356 Box::pin(self.execute_stream_with_schema_boxed_body())
4357 }
4358
4359 async fn execute_stream_with_schema_boxed_body(&self) -> SqlResult<(SchemaRef, SqlStream)> {
4360 let df_stream = self.dataframe.clone().execute_stream().await?;
4361 let schema = df_stream.schema();
4362 use futures::StreamExt;
4363 let mapped = df_stream.map(|res| {
4364 res.map_err(|e| SqlError::DataFusion {
4365 message: e.to_string(),
4366 })
4367 });
4368 Ok((schema, Box::pin(mapped)))
4369 }
4370
4371 pub fn collect_with_stats(
4379 &self,
4380 ) -> futures::future::BoxFuture<'_, SqlResult<(Vec<RecordBatch>, SqlExecutionStats)>> {
4381 Box::pin(self.collect_with_stats_boxed_body())
4382 }
4383
4384 async fn collect_with_stats_boxed_body(
4385 &self,
4386 ) -> SqlResult<(Vec<RecordBatch>, SqlExecutionStats)> {
4387 use datafusion::physical_plan::collect as df_collect;
4388
4389 let df = self.dataframe.clone();
4390 let task_ctx = df.task_ctx();
4391 let physical_plan = df.create_physical_plan().await?;
4392
4393 let batches = df_collect(physical_plan.clone(), task_ctx.into()).await?;
4394
4395 let mut output_rows: u64 = batches.iter().map(|b| b.num_rows() as u64).sum();
4396 let mut cpu_nanos: u64 = 0;
4397
4398 if let Some(metrics) = physical_plan.metrics() {
4399 if let Some(v) = metrics.output_rows() {
4400 output_rows = v as u64;
4401 }
4402 if let Some(t) = metrics.elapsed_compute() {
4403 cpu_nanos = t as u64;
4404 }
4405 }
4406
4407 let (spill_bytes, spill_count) = aggregate_spill_metrics(physical_plan.as_ref());
4408
4409 Ok((
4410 batches,
4411 SqlExecutionStats {
4412 output_rows,
4413 cpu_nanos,
4414 spill_bytes,
4415 spill_count,
4416 },
4417 ))
4418 }
4419
4420 pub fn execute_stream_with_stats(
4430 &self,
4431 ) -> futures::future::BoxFuture<'_, SqlResult<(SqlStream, SqlStatsHandle)>> {
4432 Box::pin(self.execute_stream_with_stats_boxed_body())
4433 }
4434
4435 async fn execute_stream_with_stats_boxed_body(&self) -> SqlResult<(SqlStream, SqlStatsHandle)> {
4436 use futures::StreamExt;
4437
4438 let df = self.dataframe.clone();
4439 let task_ctx = df.task_ctx();
4440 let physical_plan = df.create_physical_plan().await?;
4441 let df_stream = datafusion::physical_plan::execute_stream(
4442 physical_plan.clone(),
4443 std::sync::Arc::new(task_ctx),
4444 )?;
4445 let mapped = df_stream.map(|res| {
4446 res.map_err(|e| SqlError::DataFusion {
4447 message: e.to_string(),
4448 })
4449 });
4450 Ok((
4451 Box::pin(mapped),
4452 SqlStatsHandle {
4453 plan: physical_plan,
4454 },
4455 ))
4456 }
4457}
4458
4459pub struct SqlStatsHandle {
4462 plan: std::sync::Arc<dyn datafusion::physical_plan::ExecutionPlan>,
4463}
4464
4465impl SqlStatsHandle {
4466 pub fn stats(&self) -> SqlExecutionStats {
4471 let mut output_rows: u64 = 0;
4472 let mut cpu_nanos: u64 = 0;
4473 if let Some(metrics) = self.plan.metrics() {
4474 if let Some(v) = metrics.output_rows() {
4475 output_rows = v as u64;
4476 }
4477 if let Some(t) = metrics.elapsed_compute() {
4478 cpu_nanos = t as u64;
4479 }
4480 }
4481 let (spill_bytes, spill_count) = aggregate_spill_metrics(self.plan.as_ref());
4482 SqlExecutionStats {
4483 output_rows,
4484 cpu_nanos,
4485 spill_bytes,
4486 spill_count,
4487 }
4488 }
4489}
4490
4491fn aggregate_spill_metrics(plan: &dyn datafusion::physical_plan::ExecutionPlan) -> (u64, u64) {
4498 let mut spill_bytes: u64 = 0;
4499 let mut spill_count: u64 = 0;
4500 if let Some(metrics) = plan.metrics() {
4501 if let Some(bytes) = metrics.spilled_bytes() {
4502 spill_bytes = spill_bytes.saturating_add(bytes as u64);
4503 }
4504 if let Some(count) = metrics.spill_count() {
4505 spill_count = spill_count.saturating_add(count as u64);
4506 }
4507 }
4508 for child in plan.children() {
4509 let (child_bytes, child_count) = aggregate_spill_metrics(child.as_ref());
4510 spill_bytes = spill_bytes.saturating_add(child_bytes);
4511 spill_count = spill_count.saturating_add(child_count);
4512 }
4513 (spill_bytes, spill_count)
4514}
4515
4516#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4518pub struct SqlExecutionStats {
4519 pub output_rows: u64,
4520 pub cpu_nanos: u64,
4521 pub spill_bytes: u64,
4523 pub spill_count: u64,
4525}
4526
4527fn top_level_alias_index(expression: &str) -> Option<usize> {
4528 let bytes = expression.as_bytes();
4529 let mut depth = 0usize;
4530 let mut single_quoted = false;
4531 let mut double_quoted = false;
4532 let mut candidate = None;
4533 let mut index = 0usize;
4534 while index < bytes.len() {
4535 let Some(&byte) = bytes.get(index) else {
4536 break;
4537 };
4538 match byte {
4539 b'\'' if !double_quoted => {
4540 if single_quoted && bytes.get(index + 1) == Some(&b'\'') {
4541 index += 2;
4542 continue;
4543 }
4544 single_quoted = !single_quoted;
4545 }
4546 b'"' if !single_quoted => {
4547 if double_quoted && bytes.get(index + 1) == Some(&b'"') {
4548 index += 2;
4549 continue;
4550 }
4551 double_quoted = !double_quoted;
4552 }
4553 b'(' if !single_quoted && !double_quoted => depth += 1,
4554 b')' if !single_quoted && !double_quoted => depth = depth.saturating_sub(1),
4555 b' ' if depth == 0
4556 && !single_quoted
4557 && !double_quoted
4558 && bytes
4559 .get(index..index + 4)
4560 .is_some_and(|slice| slice.eq_ignore_ascii_case(b" AS ")) =>
4561 {
4562 candidate = Some(index);
4563 index += 3;
4564 }
4565 _ => {}
4566 }
4567 index += 1;
4568 }
4569 candidate
4570}
4571
4572fn parse_dataframe_expression(
4573 dataframe: &datafusion::dataframe::DataFrame,
4574 expression: &str,
4575) -> SqlResult<datafusion::logical_expr::Expr> {
4576 if let Some(index) = top_level_alias_index(expression) {
4577 let (body, alias) = expression.split_at(index);
4578 let alias = alias[4..].trim();
4579 if !alias.is_empty() {
4580 let alias = alias
4581 .strip_prefix('"')
4582 .and_then(|value| value.strip_suffix('"'))
4583 .unwrap_or(alias)
4584 .replace("\"\"", "\"");
4585 return Ok(dataframe.parse_sql_expr(body.trim())?.alias(alias));
4586 }
4587 }
4588 dataframe.parse_sql_expr(expression).map_err(Into::into)
4589}
4590
4591pub fn parse_public_expression(sql: &str) -> SqlResult<krishiv_plan::expression::Expr> {
4593 let dialect = GenericDialect {};
4594 let mut parser =
4595 Parser::new(&dialect)
4596 .try_with_sql(sql)
4597 .map_err(|error| SqlError::Unsupported {
4598 feature: format!("public expression parse: {error}"),
4599 })?;
4600 let expression = parser.parse_expr().map_err(|error| SqlError::Unsupported {
4601 feature: format!("public expression parse: {error}"),
4602 })?;
4603 sqlparser_expression_to_public(&expression)
4604}
4605
4606fn sqlparser_expression_to_public(
4607 expression: &datafusion::sql::sqlparser::ast::Expr,
4608) -> SqlResult<krishiv_plan::expression::Expr> {
4609 use datafusion::sql::sqlparser::ast::{BinaryOperator as SqlOperator, Expr as SqlExpr, Value};
4610 use krishiv_plan::expression::{BinaryOperator, Expr, ScalarValue};
4611
4612 Ok(match expression {
4613 SqlExpr::Identifier(identifier) => Expr::Column {
4614 path: vec![identifier.value.clone()],
4615 },
4616 SqlExpr::CompoundIdentifier(identifiers) => Expr::Column {
4617 path: identifiers
4618 .iter()
4619 .map(|identifier| identifier.value.clone())
4620 .collect(),
4621 },
4622 SqlExpr::Nested(expression) => sqlparser_expression_to_public(expression)?,
4623 SqlExpr::IsNull(expression) => Expr::IsNull {
4624 expression: Box::new(sqlparser_expression_to_public(expression)?),
4625 negated: false,
4626 },
4627 SqlExpr::IsNotNull(expression) => Expr::IsNull {
4628 expression: Box::new(sqlparser_expression_to_public(expression)?),
4629 negated: true,
4630 },
4631 SqlExpr::BinaryOp { left, op, right } => Expr::Binary {
4632 left: Box::new(sqlparser_expression_to_public(left)?),
4633 op: match op {
4634 SqlOperator::Eq => BinaryOperator::Eq,
4635 SqlOperator::NotEq => BinaryOperator::NotEq,
4636 SqlOperator::Gt => BinaryOperator::Gt,
4637 SqlOperator::GtEq => BinaryOperator::GtEq,
4638 SqlOperator::Lt => BinaryOperator::Lt,
4639 SqlOperator::LtEq => BinaryOperator::LtEq,
4640 SqlOperator::And => BinaryOperator::And,
4641 SqlOperator::Or => BinaryOperator::Or,
4642 SqlOperator::Plus => BinaryOperator::Plus,
4643 SqlOperator::Minus => BinaryOperator::Minus,
4644 SqlOperator::Multiply => BinaryOperator::Multiply,
4645 SqlOperator::Divide => BinaryOperator::Divide,
4646 other => {
4647 return Err(SqlError::Unsupported {
4648 feature: format!("public expression operator {other}"),
4649 });
4650 }
4651 },
4652 right: Box::new(sqlparser_expression_to_public(right)?),
4653 },
4654 SqlExpr::Value(value) => Expr::Literal {
4655 value: match &value.value {
4656 Value::Null => ScalarValue::Null,
4657 Value::Boolean(value) => ScalarValue::Boolean(*value),
4658 Value::SingleQuotedString(value) => ScalarValue::Utf8(value.clone()),
4659 Value::Number(value, _)
4660 if value.contains('.') || value.contains('e') || value.contains('E') =>
4661 {
4662 ScalarValue::float64(value.parse::<f64>().map_err(|error| {
4663 SqlError::Unsupported {
4664 feature: format!("numeric expression literal: {error}"),
4665 }
4666 })?)
4667 }
4668 Value::Number(value, _) => {
4669 ScalarValue::Int64(value.parse::<i64>().map_err(|error| {
4670 SqlError::Unsupported {
4671 feature: format!("integer expression literal: {error}"),
4672 }
4673 })?)
4674 }
4675 other => {
4676 return Err(SqlError::Unsupported {
4677 feature: format!("public expression literal {other}"),
4678 });
4679 }
4680 },
4681 },
4682 other => {
4683 return Err(SqlError::Unsupported {
4684 feature: format!("public expression node {other}"),
4685 });
4686 }
4687 })
4688}
4689
4690fn public_data_type_to_arrow(
4691 data_type: &krishiv_plan::expression::ExprDataType,
4692) -> arrow::datatypes::DataType {
4693 use arrow::datatypes::{DataType, Field, IntervalUnit, TimeUnit};
4694 use krishiv_plan::expression::{ExprDataType, IntervalUnit as PublicIntervalUnit};
4695
4696 match data_type {
4697 ExprDataType::Null => DataType::Null,
4698 ExprDataType::Boolean => DataType::Boolean,
4699 ExprDataType::Int64 => DataType::Int64,
4700 ExprDataType::UInt64 => DataType::UInt64,
4701 ExprDataType::Float64 => DataType::Float64,
4702 ExprDataType::Utf8 => DataType::Utf8,
4703 ExprDataType::Binary => DataType::Binary,
4704 ExprDataType::Decimal128 { precision, scale } => DataType::Decimal128(*precision, *scale),
4705 ExprDataType::Date32 => DataType::Date32,
4706 ExprDataType::Timestamp { unit, timezone } => DataType::Timestamp(
4707 match unit {
4708 krishiv_plan::expression::TimeUnit::Second => TimeUnit::Second,
4709 krishiv_plan::expression::TimeUnit::Millisecond => TimeUnit::Millisecond,
4710 krishiv_plan::expression::TimeUnit::Microsecond => TimeUnit::Microsecond,
4711 krishiv_plan::expression::TimeUnit::Nanosecond => TimeUnit::Nanosecond,
4712 },
4713 timezone.clone().map(Into::into),
4714 ),
4715 ExprDataType::Interval { unit } => DataType::Interval(match unit {
4716 PublicIntervalUnit::YearMonth => IntervalUnit::YearMonth,
4717 PublicIntervalUnit::DayTime => IntervalUnit::DayTime,
4718 PublicIntervalUnit::MonthDayNano => IntervalUnit::MonthDayNano,
4719 }),
4720 ExprDataType::List(element) => DataType::List(Arc::new(Field::new(
4721 "item",
4722 public_data_type_to_arrow(element),
4723 true,
4724 ))),
4725 ExprDataType::Map { key, value } => DataType::Map(
4726 Arc::new(Field::new(
4727 "entries",
4728 DataType::Struct(
4729 vec![
4730 Arc::new(Field::new("key", public_data_type_to_arrow(key), false)),
4731 Arc::new(Field::new("value", public_data_type_to_arrow(value), true)),
4732 ]
4733 .into(),
4734 ),
4735 false,
4736 )),
4737 false,
4738 ),
4739 ExprDataType::Struct(fields) => DataType::Struct(
4740 fields
4741 .iter()
4742 .map(|field| {
4743 Arc::new(Field::new(
4744 &field.name,
4745 public_data_type_to_arrow(&field.data_type),
4746 field.nullable,
4747 ))
4748 })
4749 .collect::<Vec<_>>()
4750 .into(),
4751 ),
4752 ExprDataType::Variant => DataType::Utf8,
4757 }
4758}
4759
4760fn public_scalar_to_datafusion(
4761 value: &krishiv_plan::expression::ScalarValue,
4762) -> Option<datafusion::common::ScalarValue> {
4763 use datafusion::common::ScalarValue;
4764 use krishiv_plan::expression::{ScalarValue as PublicScalar, TimeUnit};
4765
4766 Some(match value {
4767 PublicScalar::Null => ScalarValue::Null,
4768 PublicScalar::Boolean(value) => ScalarValue::Boolean(Some(*value)),
4769 PublicScalar::Int64(value) => ScalarValue::Int64(Some(*value)),
4770 PublicScalar::UInt64(value) => ScalarValue::UInt64(Some(*value)),
4771 PublicScalar::Float64(bits) => ScalarValue::Float64(Some(f64::from_bits(*bits))),
4772 PublicScalar::Utf8(value) => ScalarValue::Utf8(Some(value.clone())),
4773 PublicScalar::Binary(value) => ScalarValue::Binary(Some(value.clone())),
4774 PublicScalar::Decimal128 {
4775 value,
4776 precision,
4777 scale,
4778 } => ScalarValue::Decimal128(Some(*value), *precision, *scale),
4779 PublicScalar::Date32(value) => ScalarValue::Date32(Some(*value)),
4780 PublicScalar::Timestamp {
4781 value,
4782 unit,
4783 timezone,
4784 } => {
4785 let timezone = timezone.clone().map(Into::into);
4786 match unit {
4787 TimeUnit::Second => ScalarValue::TimestampSecond(Some(*value), timezone),
4788 TimeUnit::Millisecond => ScalarValue::TimestampMillisecond(Some(*value), timezone),
4789 TimeUnit::Microsecond => ScalarValue::TimestampMicrosecond(Some(*value), timezone),
4790 TimeUnit::Nanosecond => ScalarValue::TimestampNanosecond(Some(*value), timezone),
4791 }
4792 }
4793 PublicScalar::Interval { .. } => return None,
4794 })
4795}
4796
4797fn lower_public_expression(
4803 dataframe: &datafusion::dataframe::DataFrame,
4804 expression: &krishiv_plan::expression::Expr,
4805) -> SqlResult<datafusion::logical_expr::Expr> {
4806 expression
4807 .validate()
4808 .map_err(|error| SqlError::Unsupported {
4809 feature: format!("invalid public expression: {error}"),
4810 })?;
4811 use datafusion::logical_expr::{Expr as DataFusionExpr, Operator, binary_expr, cast, try_cast};
4812 use krishiv_plan::expression::{BinaryOperator, Expr};
4813
4814 Ok(match expression {
4815 Expr::Column { path } if path.len() == 1 => {
4816 datafusion::prelude::col(path.first().map(String::as_str).unwrap_or(""))
4817 }
4818 Expr::Column { .. } => parse_dataframe_expression(dataframe, &expression.to_sql())?,
4819 Expr::Literal { value } => match public_scalar_to_datafusion(value) {
4820 Some(value) => DataFusionExpr::Literal(value, None),
4821 None => parse_dataframe_expression(dataframe, &expression.to_sql())?,
4822 },
4823 Expr::Alias { expression, name } => {
4824 lower_public_expression(dataframe, expression)?.alias(name)
4825 }
4826 Expr::Binary { left, op, right } => binary_expr(
4827 lower_public_expression(dataframe, left)?,
4828 match op {
4829 BinaryOperator::Eq => Operator::Eq,
4830 BinaryOperator::NotEq => Operator::NotEq,
4831 BinaryOperator::Gt => Operator::Gt,
4832 BinaryOperator::GtEq => Operator::GtEq,
4833 BinaryOperator::Lt => Operator::Lt,
4834 BinaryOperator::LtEq => Operator::LtEq,
4835 BinaryOperator::And => Operator::And,
4836 BinaryOperator::Or => Operator::Or,
4837 BinaryOperator::Plus => Operator::Plus,
4838 BinaryOperator::Minus => Operator::Minus,
4839 BinaryOperator::Multiply => Operator::Multiply,
4840 BinaryOperator::Divide => Operator::Divide,
4841 },
4842 lower_public_expression(dataframe, right)?,
4843 ),
4844 Expr::IsNull {
4845 expression,
4846 negated,
4847 } => {
4848 let expression = lower_public_expression(dataframe, expression)?;
4849 if *negated {
4850 expression.is_not_null()
4851 } else {
4852 expression.is_null()
4853 }
4854 }
4855 Expr::Cast {
4856 expression,
4857 data_type,
4858 safe,
4859 } => {
4860 let expression = lower_public_expression(dataframe, expression)?;
4861 let data_type = public_data_type_to_arrow(data_type);
4862 if *safe {
4863 try_cast(expression, data_type)
4864 } else {
4865 cast(expression, data_type)
4866 }
4867 }
4868 Expr::Sort { .. } => {
4869 return Err(SqlError::Unsupported {
4870 feature: "standalone sort expressions are only valid inside windows or order_by"
4871 .into(),
4872 });
4873 }
4874 Expr::Aggregate { .. }
4875 | Expr::Function { .. }
4876 | Expr::Window { .. }
4877 | Expr::RawSql { .. } => parse_dataframe_expression(dataframe, &expression.to_sql())?,
4878 })
4879}
4880
4881fn sql_dataframe<'a>(
4882 dataframe: &'a dyn KrishivDataFrameOps,
4883 operation: &str,
4884) -> SqlResult<&'a SqlDataFrame> {
4885 dataframe
4886 .as_any()
4887 .downcast_ref::<SqlDataFrame>()
4888 .ok_or_else(|| SqlError::DataFusion {
4889 message: format!("right DataFrame must be SqlDataFrame for {operation}"),
4890 })
4891}
4892
4893#[async_trait::async_trait]
4894impl KrishivDataFrameOps for SqlDataFrame {
4895 async fn collect(&self) -> SqlResult<Vec<RecordBatch>> {
4896 SqlDataFrame::collect(self).await
4897 }
4898 async fn collect_with_stats(&self) -> SqlResult<(Vec<RecordBatch>, SqlExecutionStats)> {
4899 SqlDataFrame::collect_with_stats(self).await
4900 }
4901 async fn explain_analyze(&self) -> SqlResult<String> {
4902 SqlDataFrame::explain_analyze(self).await
4903 }
4904
4905 async fn explain(&self) -> SqlResult<String> {
4906 SqlDataFrame::explain(self).await
4907 }
4908 fn explain_logical(&self) -> String {
4909 SqlDataFrame::explain_logical(self)
4910 }
4911 fn krishiv_logical_plan(&self) -> LogicalPlan {
4912 let label = self.dataframe.logical_plan().to_string();
4913 let mut plan = LogicalPlan::new(self.name.clone(), ExecutionKind::Batch).with_node(
4914 PlanNode::new("datafusion-logical", label, ExecutionKind::Batch),
4915 );
4916 if let Some(n) = self.shuffle_partitions {
4917 plan = plan.with_shuffle_partitions(Some(n));
4918 }
4919 plan
4920 }
4921 fn query(&self) -> Option<&str> {
4922 SqlDataFrame::query(self)
4923 }
4924 fn to_sql(&self) -> SqlResult<String> {
4925 match datafusion::sql::unparser::plan_to_sql(self.dataframe.logical_plan()) {
4928 Ok(statement) => Ok(statement.to_string()),
4929 Err(err) => self
4930 .query()
4931 .map(str::to_string)
4932 .ok_or_else(|| SqlError::Unsupported {
4933 feature: format!("cannot render DataFrame plan as SQL: {err}"),
4934 }),
4935 }
4936 }
4937 async fn execute_stream(&self) -> SqlResult<SqlStream> {
4938 SqlDataFrame::execute_stream(self).await
4939 }
4940
4941 fn schema(&self) -> SchemaRef {
4944 SchemaRef::from(self.dataframe.schema().clone())
4945 }
4946
4947 async fn select(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4948 let df = self.dataframe.clone().select_columns(columns)?;
4949 Ok(Box::new(self.with_new_dataframe(df, "select")))
4950 }
4951
4952 async fn select_exprs(
4953 &self,
4954 expressions: &[&krishiv_plan::expression::Expr],
4955 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4956 let expressions = expressions
4957 .iter()
4958 .map(|expression| lower_public_expression(&self.dataframe, expression))
4959 .collect::<Result<Vec<_>, _>>()?;
4960 let df = self.dataframe.clone().select(expressions)?;
4961 Ok(Box::new(self.with_new_dataframe(df, "select_exprs")))
4962 }
4963
4964 async fn unnest_columns(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4965 let df = self.dataframe.clone().unnest_columns(columns)?;
4966 Ok(Box::new(self.with_new_dataframe(df, "unnest")))
4967 }
4968
4969 async fn aggregate(
4970 &self,
4971 group_exprs: &[&krishiv_plan::expression::Expr],
4972 aggregate_exprs: &[&krishiv_plan::expression::Expr],
4973 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4974 if aggregate_exprs.is_empty() {
4975 return Err(SqlError::Unsupported {
4976 feature: "aggregate requires at least one aggregate expression".into(),
4977 });
4978 }
4979 let group_exprs = group_exprs
4980 .iter()
4981 .map(|expression| lower_public_expression(&self.dataframe, expression))
4982 .collect::<Result<Vec<_>, _>>()?;
4983 let aggregate_exprs = aggregate_exprs
4984 .iter()
4985 .map(|expression| lower_public_expression(&self.dataframe, expression))
4986 .collect::<Result<Vec<_>, _>>()?;
4987 let df = self
4988 .dataframe
4989 .clone()
4990 .aggregate(group_exprs, aggregate_exprs)?;
4991 Ok(Box::new(self.with_new_dataframe(df, "aggregate")))
4992 }
4993
4994 async fn aggregate_grouping(
4995 &self,
4996 grouping: GroupingMode<'_>,
4997 aggregate_exprs: &[&krishiv_plan::expression::Expr],
4998 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4999 if aggregate_exprs.is_empty() {
5000 return Err(SqlError::Unsupported {
5001 feature: "grouping aggregation requires at least one aggregate expression".into(),
5002 });
5003 }
5004 let lower = |expression: &&krishiv_plan::expression::Expr| {
5005 lower_public_expression(&self.dataframe, expression)
5006 };
5007 let group = match grouping {
5008 GroupingMode::Sets(sets) => datafusion::logical_expr::grouping_set(
5009 sets.into_iter()
5010 .map(|set| set.iter().map(lower).collect::<Result<Vec<_>, _>>())
5011 .collect::<Result<Vec<_>, _>>()?,
5012 ),
5013 GroupingMode::Cube(expressions) => datafusion::logical_expr::cube(
5014 expressions
5015 .iter()
5016 .map(lower)
5017 .collect::<Result<Vec<_>, _>>()?,
5018 ),
5019 GroupingMode::Rollup(expressions) => datafusion::logical_expr::rollup(
5020 expressions
5021 .iter()
5022 .map(lower)
5023 .collect::<Result<Vec<_>, _>>()?,
5024 ),
5025 };
5026 let aggregates = aggregate_exprs
5027 .iter()
5028 .map(lower)
5029 .collect::<Result<Vec<_>, _>>()?;
5030 let df = self.dataframe.clone().aggregate(vec![group], aggregates)?;
5031 Ok(Box::new(self.with_new_dataframe(df, "aggregate_grouping")))
5032 }
5033
5034 async fn pivot(
5035 &self,
5036 group_exprs: &[&krishiv_plan::expression::Expr],
5037 pivot_column: &krishiv_plan::expression::Expr,
5038 aggregate_expr: &krishiv_plan::expression::Expr,
5039 values: &[(krishiv_plan::expression::ScalarValue, String)],
5040 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5041 use krishiv_plan::expression::Expr as PublicExpr;
5042 let (function, input, distinct) = match aggregate_expr {
5043 PublicExpr::Aggregate {
5044 function,
5045 expression: Some(input),
5046 distinct,
5047 } => (*function, input.as_ref(), *distinct),
5048 _ => {
5049 return Err(SqlError::Unsupported {
5050 feature: "pivot requires an aggregate expression with one input".into(),
5051 });
5052 }
5053 };
5054 if values.is_empty() {
5055 return Err(SqlError::Unsupported {
5056 feature: "pivot requires at least one value".into(),
5057 });
5058 }
5059 let group_exprs = group_exprs
5060 .iter()
5061 .map(|expression| lower_public_expression(&self.dataframe, expression))
5062 .collect::<Result<Vec<_>, _>>()?;
5063 let aggregates = values
5064 .iter()
5065 .map(|(value, alias)| {
5066 let conditional = PublicExpr::raw(format!(
5067 "CASE WHEN {} = {} THEN {} END",
5068 pivot_column.to_sql(),
5069 value.to_sql_literal(),
5070 input.to_sql()
5071 ));
5072 let aggregate = PublicExpr::Aggregate {
5073 function,
5074 expression: Some(Box::new(conditional)),
5075 distinct,
5076 }
5077 .alias(alias);
5078 lower_public_expression(&self.dataframe, &aggregate)
5079 })
5080 .collect::<Result<Vec<_>, _>>()?;
5081 let dataframe = self.dataframe.clone().aggregate(group_exprs, aggregates)?;
5082 Ok(Box::new(self.with_new_dataframe(dataframe, "pivot")))
5083 }
5084
5085 async fn unpivot(
5086 &self,
5087 columns: &[&str],
5088 name_column: &str,
5089 value_column: &str,
5090 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5091 if columns.is_empty() {
5092 return Err(SqlError::Unsupported {
5093 feature: "unpivot requires at least one column".into(),
5094 });
5095 }
5096 let retained = self
5097 .dataframe
5098 .schema()
5099 .fields()
5100 .iter()
5101 .map(|field| field.name().as_str())
5102 .filter(|name| !columns.contains(name))
5103 .collect::<Vec<_>>();
5104 let mut branches = Vec::with_capacity(columns.len());
5105 for column in columns {
5106 let mut expressions = retained
5107 .iter()
5108 .map(|name| datafusion::logical_expr::col(*name))
5109 .collect::<Vec<_>>();
5110 expressions
5111 .push(datafusion::logical_expr::lit((*column).to_owned()).alias(name_column));
5112 expressions.push(datafusion::logical_expr::col(*column).alias(value_column));
5113 branches.push(self.dataframe.clone().select(expressions)?);
5114 }
5115 let mut branches = branches.into_iter();
5116 let Some(mut dataframe) = branches.next() else {
5117 return Err(SqlError::Unsupported {
5118 feature: "unpivot requires at least one branch".into(),
5119 });
5120 };
5121 for branch in branches {
5122 dataframe = dataframe.union(branch)?;
5123 }
5124 Ok(Box::new(self.with_new_dataframe(dataframe, "unpivot")))
5125 }
5126
5127 async fn filter(&self, predicate: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5128 let expr = self.dataframe.parse_sql_expr(predicate)?;
5129 let df = self.dataframe.clone().filter(expr)?;
5130 Ok(Box::new(self.with_new_dataframe(df, "filter")))
5131 }
5132
5133 async fn filter_expr(
5134 &self,
5135 predicate: &krishiv_plan::expression::Expr,
5136 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5137 let expr = lower_public_expression(&self.dataframe, predicate)?;
5138 let df = self.dataframe.clone().filter(expr)?;
5139 Ok(Box::new(self.with_new_dataframe(df, "filter_expr")))
5140 }
5141
5142 async fn limit(&self, n: usize) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5143 let df = self.dataframe.clone().limit(0, Some(n))?;
5144 Ok(Box::new(self.with_new_dataframe(df, "limit")))
5145 }
5146
5147 async fn distinct(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5148 let df = self.dataframe.clone().distinct()?;
5149 Ok(Box::new(self.with_new_dataframe(df, "distinct")))
5150 }
5151
5152 async fn drop_nulls(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5153 let columns = if columns.is_empty() {
5154 self.dataframe
5155 .schema()
5156 .fields()
5157 .iter()
5158 .map(|field| field.name().as_str())
5159 .collect::<Vec<_>>()
5160 } else {
5161 columns.to_vec()
5162 };
5163 let mut predicate: Option<datafusion::logical_expr::Expr> = None;
5164 for column in columns {
5165 let next = datafusion::logical_expr::col(column).is_not_null();
5166 predicate = Some(match predicate {
5167 Some(current) => current.and(next),
5168 None => next,
5169 });
5170 }
5171 let df = match predicate {
5172 Some(predicate) => self.dataframe.clone().filter(predicate)?,
5173 None => self.dataframe.clone(),
5174 };
5175 Ok(Box::new(self.with_new_dataframe(df, "drop_nulls")))
5176 }
5177
5178 async fn sample(&self, fraction: f64) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5179 if !(0.0..=1.0).contains(&fraction) {
5180 return Err(SqlError::Unsupported {
5181 feature: "sample fraction must be between 0 and 1".into(),
5182 });
5183 }
5184 let predicate = self
5185 .dataframe
5186 .parse_sql_expr(&format!("random() < {fraction}"))?;
5187 let df = self.dataframe.clone().filter(predicate)?;
5188 Ok(Box::new(self.with_new_dataframe(df, "sample")))
5189 }
5190
5191 async fn sort(
5192 &self,
5193 columns: &[&str],
5194 descending: &[bool],
5195 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5196 use datafusion::logical_expr::SortExpr;
5197 let exprs: Vec<SortExpr> = columns
5198 .iter()
5199 .zip(descending.iter())
5200 .map(|(col_name, desc)| datafusion::logical_expr::col(*col_name).sort(!desc, *desc))
5201 .collect();
5202 let df = self.dataframe.clone().sort(exprs)?;
5203 Ok(Box::new(self.with_new_dataframe(df, "sort")))
5204 }
5205
5206 async fn alias(&self, alias: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5207 let df = self.dataframe.clone().alias(alias)?;
5208 Ok(Box::new(self.with_new_dataframe(df, "alias")))
5209 }
5210
5211 async fn drop_columns(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5212 let df = self.dataframe.clone().drop_columns(columns)?;
5213 Ok(Box::new(self.with_new_dataframe(df, "drop")))
5214 }
5215
5216 async fn rename_column(&self, old: &str, new: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5217 let df = self.dataframe.clone().with_column_renamed(old, new)?;
5218 Ok(Box::new(self.with_new_dataframe(df, "rename")))
5219 }
5220
5221 async fn with_column(&self, name: &str, expr: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5222 let parsed = self.dataframe.parse_sql_expr(expr)?;
5223 let df = self.dataframe.clone().with_column(name, parsed)?;
5224 Ok(Box::new(self.with_new_dataframe(df, "with_column")))
5225 }
5226
5227 fn as_any(&self) -> &dyn std::any::Any {
5228 self
5229 }
5230
5231 async fn describe(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5232 let df = self.dataframe.clone().describe().await?;
5233 Ok(Box::new(self.with_new_dataframe(df, "describe")))
5234 }
5235
5236 async fn fill_null(
5237 &self,
5238 column: &str,
5239 value: &str,
5240 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5241 let expr = format!("COALESCE({column}, {value})");
5242 let parsed = self.dataframe.parse_sql_expr(&expr)?;
5243 let df = self.dataframe.clone().with_column(column, parsed)?;
5244 Ok(Box::new(self.with_new_dataframe(df, "fill_null")))
5245 }
5246
5247 async fn join(
5248 &self,
5249 right: &dyn KrishivDataFrameOps,
5250 how: &str,
5251 left_on: &[&str],
5252 right_on: &[&str],
5253 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5254 let right_sql = right
5255 .as_any()
5256 .downcast_ref::<SqlDataFrame>()
5257 .ok_or_else(|| SqlError::DataFusion {
5258 message: "right DataFrame must be SqlDataFrame for join".into(),
5259 })?;
5260 use datafusion::common::JoinType;
5261 let join_type = match how.to_lowercase().as_str() {
5262 "inner" => JoinType::Inner,
5263 "left" => JoinType::Left,
5264 "right" => JoinType::Right,
5265 "full" | "outer" => JoinType::Full,
5266 "leftsemi" | "left_semi" => JoinType::LeftSemi,
5267 "rightsemi" | "right_semi" => JoinType::RightSemi,
5268 "leftanti" | "left_anti" => JoinType::LeftAnti,
5269 "rightanti" | "right_anti" => JoinType::RightAnti,
5270 _ => {
5271 return Err(SqlError::DataFusion {
5272 message: format!("unsupported join type: {how}"),
5273 });
5274 }
5275 };
5276 let df = self.dataframe.clone().join(
5277 right_sql.dataframe.clone(),
5278 join_type,
5279 left_on,
5280 right_on,
5281 None,
5282 )?;
5283 Ok(Box::new(self.with_new_dataframe(df, "join")))
5284 }
5285
5286 async fn union(
5287 &self,
5288 right: &dyn KrishivDataFrameOps,
5289 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5290 let right_sql = right
5291 .as_any()
5292 .downcast_ref::<SqlDataFrame>()
5293 .ok_or_else(|| SqlError::DataFusion {
5294 message: "right DataFrame must be SqlDataFrame for union".into(),
5295 })?;
5296 let df = self.dataframe.clone().union(right_sql.dataframe.clone())?;
5297 Ok(Box::new(self.with_new_dataframe(df, "union")))
5298 }
5299
5300 async fn union_distinct(
5301 &self,
5302 right: &dyn KrishivDataFrameOps,
5303 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5304 let right = sql_dataframe(right, "union_distinct")?;
5305 let df = self
5306 .dataframe
5307 .clone()
5308 .union_distinct(right.dataframe.clone())?;
5309 Ok(Box::new(self.with_new_dataframe(df, "union_distinct")))
5310 }
5311
5312 async fn intersect(
5313 &self,
5314 right: &dyn KrishivDataFrameOps,
5315 distinct: bool,
5316 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5317 let right = sql_dataframe(right, "intersect")?;
5318 let df = if distinct {
5319 self.dataframe
5320 .clone()
5321 .intersect_distinct(right.dataframe.clone())?
5322 } else {
5323 self.dataframe.clone().intersect(right.dataframe.clone())?
5324 };
5325 Ok(Box::new(self.with_new_dataframe(df, "intersect")))
5326 }
5327
5328 async fn except(
5329 &self,
5330 right: &dyn KrishivDataFrameOps,
5331 distinct: bool,
5332 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5333 let right = sql_dataframe(right, "except")?;
5334 let df = if distinct {
5335 self.dataframe
5336 .clone()
5337 .except_distinct(right.dataframe.clone())?
5338 } else {
5339 self.dataframe.clone().except(right.dataframe.clone())?
5340 };
5341 Ok(Box::new(self.with_new_dataframe(df, "except")))
5342 }
5343
5344 async fn register_batches(&self, name: &str, batches: Vec<RecordBatch>) -> SqlResult<()> {
5345 let schema = batches
5346 .first()
5347 .map(|b| b.schema())
5348 .unwrap_or_else(|| Arc::new(arrow::datatypes::Schema::empty()));
5349 let mem_table =
5350 datafusion::datasource::MemTable::try_new(schema, vec![batches]).map_err(|e| {
5351 SqlError::DataFusion {
5352 message: e.to_string(),
5353 }
5354 })?;
5355 self.context
5356 .register_table(name, Arc::new(mem_table))
5357 .map_err(SqlError::from)?;
5358 Ok(())
5359 }
5360
5361 async fn deregister_table(&self, name: &str) -> SqlResult<()> {
5362 let _ = self
5363 .context
5364 .deregister_table(name)
5365 .map_err(SqlError::from)?;
5366 Ok(())
5367 }
5368
5369 async fn create_view(&self, name: &str, replace: bool) -> SqlResult<()> {
5370 let query = self
5371 .query_text
5372 .as_deref()
5373 .ok_or_else(|| SqlError::DataFusion {
5374 message: "create_view requires an SQL query string on the DataFrame".into(),
5375 })?;
5376 let or_replace = if replace { "OR REPLACE " } else { "" };
5377 let safe_name = quote_identifier(name);
5378 let view_sql = format!("CREATE {or_replace}VIEW {safe_name} AS {query}");
5379 self.context.sql(&view_sql).await?;
5380 Ok(())
5381 }
5382}
5383
5384use krishiv_common::sql_util::quote_identifier;
5385
5386#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5393fn call_args_from_str(s: &str) -> Vec<String> {
5394 let mut args: Vec<String> = Vec::new();
5395 let mut cur = String::new();
5396 let mut in_str = false;
5397 let mut after_str = false;
5398 for ch in s.chars() {
5399 if after_str {
5400 if ch == ',' {
5401 after_str = false;
5402 }
5403 continue;
5404 }
5405 if in_str {
5406 if ch == '\'' {
5407 in_str = false;
5408 after_str = true;
5409 args.push(std::mem::take(&mut cur));
5410 } else {
5411 cur.push(ch);
5412 }
5413 } else if ch == '\'' {
5414 in_str = true;
5415 } else if ch == ',' {
5416 let t = cur.trim().to_string();
5417 if !t.is_empty() {
5418 args.push(t);
5419 }
5420 cur.clear();
5421 } else {
5422 cur.push(ch);
5423 }
5424 }
5425 let t = cur.trim().to_string();
5426 if !t.is_empty() {
5427 args.push(t);
5428 }
5429 args
5430}
5431
5432#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5439fn iceberg_table_ident(table_ref: &str) -> SqlResult<iceberg::TableIdent> {
5440 let parts: Vec<&str> = table_ref.splitn(3, '.').collect();
5441 match parts.len() {
5442 2 => {
5443 let ns = iceberg::NamespaceIdent::from_vec(vec![
5444 parts.first().copied().unwrap_or("").to_string(),
5445 ])
5446 .map_err(|e| SqlError::DataFusion {
5447 message: e.to_string(),
5448 })?;
5449 Ok(iceberg::TableIdent::new(
5450 ns,
5451 parts.get(1).copied().unwrap_or("").to_string(),
5452 ))
5453 }
5454 3 => {
5455 let ns = iceberg::NamespaceIdent::from_vec(vec![
5456 parts.get(1).copied().unwrap_or("").to_string(),
5457 ])
5458 .map_err(|e| SqlError::DataFusion {
5459 message: e.to_string(),
5460 })?;
5461 Ok(iceberg::TableIdent::new(
5462 ns,
5463 parts.get(2).copied().unwrap_or("").to_string(),
5464 ))
5465 }
5466 _ => Err(SqlError::DataFusion {
5467 message: format!(
5468 "invalid table reference '{table_ref}': expected 'ns.table' or 'cat.ns.table'"
5469 ),
5470 }),
5471 }
5472}
5473
5474#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5479fn parse_call_duration(s: &str) -> SqlResult<chrono::Duration> {
5480 let s = s.trim();
5481 let mut it = s.splitn(2, ' ');
5482 let n: i64 = it
5483 .next()
5484 .and_then(|v| v.parse().ok())
5485 .ok_or_else(|| SqlError::DataFusion {
5486 message: format!("invalid duration value in '{s}'"),
5487 })?;
5488 let unit = it.next().unwrap_or("").trim().to_ascii_lowercase();
5489 match unit.trim_end_matches('s') {
5490 "day" => Ok(chrono::Duration::days(n)),
5491 "hour" => Ok(chrono::Duration::hours(n)),
5492 "week" => Ok(chrono::Duration::weeks(n)),
5493 "minute" | "min" => Ok(chrono::Duration::minutes(n)),
5494 _ => Err(SqlError::DataFusion {
5495 message: format!("unknown duration unit '{unit}' in '{s}'"),
5496 }),
5497 }
5498}
5499
5500#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5508fn parse_dml_delete(stmt: &str) -> Option<(String, String)> {
5509 use datafusion::sql::sqlparser::ast::{FromTable, Statement, TableFactor};
5510 use datafusion::sql::sqlparser::dialect::GenericDialect;
5511 use datafusion::sql::sqlparser::parser::Parser;
5512
5513 let mut stmts = Parser::parse_sql(&GenericDialect {}, stmt).ok()?;
5514 if stmts.len() != 1 {
5515 return None;
5516 }
5517 let Statement::Delete(delete) = stmts.remove(0) else {
5518 return None;
5519 };
5520 let tables = match delete.from {
5523 FromTable::WithFromKeyword(tables) | FromTable::WithoutKeyword(tables) => tables,
5524 };
5525 let first_from = tables.into_iter().next()?;
5526 let table_name = match first_from.relation {
5527 TableFactor::Table { name, .. } => name.to_string(),
5528 _ => return None,
5529 };
5530 let predicate = delete
5531 .selection
5532 .map(|e| e.to_string())
5533 .unwrap_or_else(|| "TRUE".to_string());
5534 Some((table_name, predicate))
5535}
5536
5537#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5539struct ParsedInsert {
5540 table_ref: String,
5542 columns: Vec<String>,
5548 inner_query: String,
5551}
5552
5553#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5559fn parse_dml_insert(stmt: &str) -> Option<ParsedInsert> {
5560 use datafusion::sql::sqlparser::ast::{Statement, TableObject};
5561 use datafusion::sql::sqlparser::dialect::GenericDialect;
5562 use datafusion::sql::sqlparser::parser::Parser;
5563
5564 let mut stmts = Parser::parse_sql(&GenericDialect {}, stmt).ok()?;
5565 if stmts.len() != 1 {
5566 return None;
5567 }
5568 let Statement::Insert(insert) = stmts.remove(0) else {
5569 return None;
5570 };
5571 let TableObject::TableName(name) = insert.table else {
5572 return None;
5573 };
5574 let inner_query = insert.source?.to_string();
5575 Some(ParsedInsert {
5576 table_ref: name.to_string(),
5577 columns: insert.columns.iter().map(|c| c.to_string()).collect(),
5578 inner_query,
5579 })
5580}
5581
5582#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5584struct ParsedCtas {
5585 table_ref: String,
5587 or_replace: bool,
5588 inner_query: String,
5590 partition_by: Vec<String>,
5593}
5594
5595#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5605fn extract_partitioned_by(stmt: &str) -> Option<(String, Vec<String>)> {
5606 let bytes = stmt.as_bytes();
5607 let upper = stmt.to_ascii_uppercase();
5608 let upper_bytes = upper.as_bytes();
5609 const NEEDLE: &[u8] = b"PARTITIONED";
5610
5611 fn is_ident_byte(b: u8) -> bool {
5612 b.is_ascii_alphanumeric() || b == b'_'
5613 }
5614 fn skip_quoted(bytes: &[u8], mut i: usize, quote: u8) -> usize {
5617 i += 1;
5618 while let Some(&b) = bytes.get(i) {
5619 if b == quote {
5620 if bytes.get(i + 1) == Some("e) {
5621 i += 2;
5622 continue;
5623 }
5624 return i + 1;
5625 }
5626 i += 1;
5627 }
5628 i
5629 }
5630
5631 let mut i = 0;
5632 while let Some(&b) = bytes.get(i) {
5633 match b {
5634 b'\'' | b'"' => i = skip_quoted(bytes, i, b),
5635 _ => {
5636 let at_needle = upper_bytes
5637 .get(i..)
5638 .is_some_and(|rest| rest.starts_with(NEEDLE))
5639 && (i == 0
5640 || !i
5641 .checked_sub(1)
5642 .and_then(|p| upper_bytes.get(p))
5643 .copied()
5644 .is_some_and(is_ident_byte));
5645 if at_needle {
5646 let mut j = i + NEEDLE.len();
5647 while bytes.get(j).is_some_and(u8::is_ascii_whitespace) {
5648 j += 1;
5649 }
5650 if j > i + NEEDLE.len()
5653 && upper_bytes
5654 .get(j..)
5655 .is_some_and(|rest| rest.starts_with(b"BY"))
5656 && !upper_bytes.get(j + 2).copied().is_some_and(is_ident_byte)
5657 {
5658 let mut k = j + 2;
5659 while bytes.get(k).is_some_and(u8::is_ascii_whitespace) {
5660 k += 1;
5661 }
5662 if bytes.get(k) == Some(&b'(') {
5663 let mut depth = 0i32;
5665 let mut c = k;
5666 let close = loop {
5667 match bytes.get(c) {
5668 None => return None,
5670 Some(b'(') => depth += 1,
5671 Some(b')') => {
5672 depth -= 1;
5673 if depth == 0 {
5674 break c;
5675 }
5676 }
5677 Some(&(q @ b'\'' | q @ b'"')) => {
5678 c = skip_quoted(bytes, c, q);
5679 continue;
5680 }
5681 Some(_) => {}
5682 }
5683 c += 1;
5684 };
5685 let body = stmt.get(k + 1..close)?;
5686 let head = stmt.get(..i)?.trim_end();
5687 let tail = stmt.get(close + 1..)?.trim_start();
5688 let items = split_top_level_commas(body);
5689 let mut remainder = String::with_capacity(stmt.len());
5690 remainder.push_str(head);
5691 remainder.push(' ');
5692 remainder.push_str(tail);
5693 return Some((remainder, items));
5694 }
5695 }
5696 }
5697 i += 1;
5698 }
5699 }
5700 }
5701 None
5702}
5703
5704#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5707fn split_top_level_commas(s: &str) -> Vec<String> {
5708 let bytes = s.as_bytes();
5709 let mut items = Vec::new();
5710 let mut depth = 0i32;
5711 let mut start = 0usize;
5712 let mut i = 0;
5713 while let Some(&b) = bytes.get(i) {
5714 match b {
5715 b'(' => depth += 1,
5716 b')' => depth -= 1,
5717 b'\'' | b'"' => {
5718 i += 1;
5719 while bytes.get(i).is_some_and(|&c| c != b) {
5720 i += 1;
5721 }
5722 }
5723 b',' if depth == 0 => {
5724 if let Some(item) = s.get(start..i).map(str::trim)
5725 && !item.is_empty()
5726 {
5727 items.push(item.to_string());
5728 }
5729 start = i + 1;
5730 }
5731 _ => {}
5732 }
5733 i += 1;
5734 }
5735 if let Some(last) = s.get(start..).map(str::trim)
5736 && !last.is_empty()
5737 {
5738 items.push(last.to_string());
5739 }
5740 items
5741}
5742
5743fn split_sql_statements(sql: &str) -> Vec<String> {
5751 let mut items = Vec::new();
5752 let mut start = 0usize;
5753 let mut chars = sql.char_indices().peekable();
5754 while let Some((i, c)) = chars.next() {
5755 match c {
5756 '\'' => {
5757 while let Some((_, c2)) = chars.next() {
5759 if c2 == '\'' {
5760 if chars.peek().is_some_and(|&(_, c3)| c3 == '\'') {
5761 chars.next();
5762 continue;
5763 }
5764 break;
5765 }
5766 }
5767 }
5768 '"' => {
5769 for (_, c2) in chars.by_ref() {
5770 if c2 == '"' {
5771 break;
5772 }
5773 }
5774 }
5775 '-' if chars.peek().is_some_and(|&(_, c2)| c2 == '-') => {
5776 for (_, c2) in chars.by_ref() {
5777 if c2 == '\n' {
5778 break;
5779 }
5780 }
5781 }
5782 '/' if chars.peek().is_some_and(|&(_, c2)| c2 == '*') => {
5783 chars.next();
5784 let mut star = false;
5785 for (_, c2) in chars.by_ref() {
5786 if star && c2 == '/' {
5787 break;
5788 }
5789 star = c2 == '*';
5790 }
5791 }
5792 ';' => {
5793 if let Some(piece) = sql.get(start..i).map(str::trim)
5794 && !piece.is_empty()
5795 {
5796 items.push(piece.to_string());
5797 }
5798 start = i + 1;
5800 }
5801 _ => {}
5802 }
5803 }
5804 if let Some(last) = sql.get(start..).map(str::trim)
5805 && !last.is_empty()
5806 {
5807 items.push(last.to_string());
5808 }
5809 items
5810}
5811
5812#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5820fn parse_ctas(stmt: &str) -> Option<ParsedCtas> {
5821 use datafusion::sql::sqlparser::ast::Statement;
5822 use datafusion::sql::sqlparser::dialect::GenericDialect;
5823 use datafusion::sql::sqlparser::parser::Parser;
5824
5825 let (stripped, partition_by) = match extract_partitioned_by(stmt) {
5826 Some((remainder, items)) => (remainder, items),
5827 None => (stmt.to_string(), Vec::new()),
5828 };
5829 let mut stmts = Parser::parse_sql(&GenericDialect {}, &stripped).ok()?;
5830 if stmts.len() != 1 {
5831 return None;
5832 }
5833 let Statement::CreateTable(create) = stmts.remove(0) else {
5834 return None;
5835 };
5836 if create.external || create.temporary {
5837 return None;
5838 }
5839 let inner_query = create.query?.to_string();
5840 Some(ParsedCtas {
5841 table_ref: create.name.to_string(),
5842 or_replace: create.or_replace,
5843 inner_query,
5844 partition_by,
5845 })
5846}
5847
5848#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5850struct ParsedUpdate {
5851 table_ref: String,
5852 assignments: Vec<(String, String)>,
5854 predicate: Option<String>,
5855}
5856
5857#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5863fn parse_dml_update(stmt: &str) -> Option<ParsedUpdate> {
5864 use datafusion::sql::sqlparser::ast::{Statement, TableFactor};
5865 use datafusion::sql::sqlparser::dialect::GenericDialect;
5866 use datafusion::sql::sqlparser::parser::Parser;
5867
5868 let mut stmts = Parser::parse_sql(&GenericDialect {}, stmt).ok()?;
5869 if stmts.len() != 1 {
5870 return None;
5871 }
5872 let Statement::Update(update) = stmts.remove(0) else {
5874 return None;
5875 };
5876 let table_name = match update.table.relation {
5877 TableFactor::Table { name, .. } => name.to_string(),
5878 _ => return None,
5879 };
5880 let parsed_assignments: Vec<(String, String)> = update
5882 .assignments
5883 .into_iter()
5884 .map(|a| {
5885 let col = a.target.to_string();
5887 let val = a.value.to_string();
5888 (col, val)
5889 })
5890 .collect();
5891 if parsed_assignments.is_empty() {
5892 return None;
5893 }
5894 Some(ParsedUpdate {
5895 table_ref: table_name,
5896 assignments: parsed_assignments,
5897 predicate: update.selection.map(|e| e.to_string()),
5898 })
5899}
5900
5901pub fn plan_sql(query: impl Into<String>) -> SqlResult<SqlPlan> {
5903 let query = query.into();
5904 if query.trim().is_empty() {
5905 return Err(SqlError::EmptyQuery);
5906 }
5907
5908 if let Some(stmt) = cep_sql::parse_match_recognize(&query)? {
5909 let logical_plan = cep_sql::plan_match_recognize(stmt, &query);
5910 let optimized = Optimizer::default().optimize(logical_plan)?;
5911 return Ok(SqlPlan {
5912 query,
5913 logical_plan: optimized.plan,
5914 });
5915 }
5916
5917 let logical_plan =
5918 LogicalPlan::new("sql-query", ExecutionKind::Batch).with_node(PlanNode::new(
5919 "sql",
5920 format!("sql: {}", query.trim()),
5921 ExecutionKind::Batch,
5922 ));
5923
5924 let optimized = Optimizer::default().optimize(logical_plan)?;
5925 Ok(SqlPlan {
5926 query,
5927 logical_plan: optimized.plan,
5928 })
5929}
5930
5931pub fn explain_sql(query: impl Into<String>) -> SqlResult<String> {
5933 let plan = plan_sql(query)?;
5934 Ok(plan.logical_plan().describe())
5935}
5936
5937pub fn explain_sql_optimized(query: impl Into<String>, optimizer: &Optimizer) -> SqlResult<String> {
5942 let plan = plan_sql(query)?;
5943 let result = optimizer.optimize(plan.logical_plan().clone())?;
5944 let mut output = result.plan.describe();
5945 let optimizer_line = result.describe();
5946 output.push('\n');
5947 output.push_str(&optimizer_line);
5948 Ok(output)
5949}
5950
5951pub fn explain_sql_with_cost(
5953 query: impl Into<String>,
5954 cost_model: &dyn CostModel,
5955) -> SqlResult<String> {
5956 let plan = plan_sql(query)?;
5957 let cost = cost_model.estimate(plan.logical_plan());
5958 let mut output = plan.logical_plan().describe();
5959 output.push_str(&format!(
5960 "\ncost: cpu_nanos={}, memory_bytes={}, network_bytes={}",
5961 cost.cpu_nanos, cost.memory_bytes, cost.network_bytes
5962 ));
5963 Ok(output)
5964}
5965
5966pub fn referenced_table_names(query: impl AsRef<str>) -> SqlResult<Vec<String>> {
5972 let query = query.as_ref();
5973 if query.trim().is_empty() {
5974 return Err(SqlError::EmptyQuery);
5975 }
5976
5977 let statements =
5978 Parser::parse_sql(&GenericDialect {}, query).map_err(|e| SqlError::DataFusion {
5979 message: format!("SQL parse error: {e}"),
5980 })?;
5981 let mut names = BTreeSet::new();
5982 let _ = visit_relations(&statements, |relation| {
5983 names.insert(relation.to_string());
5984 ControlFlow::<()>::Continue(())
5985 });
5986 Ok(names.into_iter().collect())
5987}
5988
5989pub fn pretty_batches(batches: &[RecordBatch]) -> SqlResult<String> {
5991 Ok(pretty_format_batches(batches)
5992 .map_err(|error| SqlError::DataFusion {
5993 message: error.to_string(),
5994 })?
5995 .to_string())
5996}
5997
5998#[cfg(test)]
5999mod sql_tests;