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 {
31 use arrow::datatypes::DataType;
32 match name.trim().to_ascii_lowercase().as_str() {
33 "double" | "float64" | "float" => DataType::Float64,
34 "float32" | "real" => DataType::Float32,
35 "int" | "int64" | "bigint" | "long" => DataType::Int64,
36 "int32" | "integer" => DataType::Int32,
37 "bool" | "boolean" => DataType::Boolean,
38 "utf8" | "string" | "varchar" | "text" => DataType::Utf8,
39 _ => DataType::Utf8,
40 }
41}
42
43pub(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 pipe_syntax;
116pub mod pivot_sql;
117pub mod python_udf;
118pub mod scalar_udf;
119pub mod semi_join_reduction;
120pub mod spark_sql_ext;
122pub mod runtime_filter_exec;
123pub mod spillable_join;
124pub mod sqlstate;
125pub mod subquery;
126pub mod unnest_sql;
127pub mod unspillable_headroom;
128
129pub mod coverage;
130mod higher_order_functions;
131mod json_functions;
132mod spark_functions;
133pub mod statement_completion;
134pub mod streaming;
135pub mod streaming_table_ddl;
136pub mod streaming_tvf;
137pub mod streaming_window_plan;
138mod udf;
139mod window_functions;
140
141pub use cep_sql::{
142 MatchRecognizeStatement, execute_streaming_match_recognize, parse_match_recognize,
143};
144pub use lakehouse::{AsOfTableRef, MergeResult, MergeTargetUnsupportedError, preprocess_as_of_sql};
145
146pub use grammar::{
147 FeatureEntry, FeatureStatus, feature_matrix, features_by_status, features_for_category,
148};
149pub use sqlstate::{SqlStateError, sqlstate_for};
150pub use streaming::{ContinuousInputError, ContinuousTableInput};
151
152pub type SqlResult<T> = Result<T, SqlError>;
154
155pub type SqlStream =
161 std::pin::Pin<Box<dyn futures::stream::Stream<Item = Result<RecordBatch, SqlError>> + Send>>;
162
163static EPHEMERAL_TABLE_COUNTER: AtomicU64 = AtomicU64::new(0);
166
167fn next_ephemeral_name(prefix: &str) -> String {
168 let id = EPHEMERAL_TABLE_COUNTER.fetch_add(1, Ordering::Relaxed);
169 format!("__{prefix}_{id}")
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177enum WindowFnRegistration {
178 Register,
180 Skip,
184}
185
186struct PlanCache {
192 map: HashMap<String, (datafusion::logical_expr::LogicalPlan, std::time::Instant)>,
193 order: VecDeque<String>,
194 max: usize,
195}
196
197const PLAN_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(30);
209
210impl PlanCache {
211 fn new(max: usize) -> Self {
212 Self {
213 map: HashMap::new(),
214 order: VecDeque::new(),
215 max,
216 }
217 }
218
219 fn get(&mut self, key: &str) -> Option<&datafusion::logical_expr::LogicalPlan> {
227 match self.map.get(key).map(|(_, at)| at.elapsed() < PLAN_CACHE_TTL) {
228 None => None,
229 Some(false) => {
230 self.map.remove(key);
231 self.order.retain(|k| k != key);
232 None
233 }
234 Some(true) => {
235 if let Some(pos) = self.order.iter().position(|k| k == key)
236 && let Some(promoted) = self.order.remove(pos)
237 {
238 self.order.push_back(promoted);
239 }
240 self.map.get(key).map(|(plan, _)| plan)
241 }
242 }
243 }
244
245 fn insert(&mut self, key: String, plan: datafusion::logical_expr::LogicalPlan) {
246 if self.map.contains_key(&key) {
247 self.order.retain(|k| k != &key);
250 } else if self.map.len() >= self.max
251 && let Some(oldest) = self.order.pop_front()
252 {
253 self.map.remove(&oldest);
254 }
255 self.order.push_back(key.clone());
256 self.map.insert(key, (plan, std::time::Instant::now()));
257 }
258
259 fn clear(&mut self) {
260 self.map.clear();
261 self.order.clear();
262 }
263
264 #[cfg(test)]
265 fn is_empty(&self) -> bool {
266 self.map.is_empty()
267 }
268}
269
270#[derive(Debug, Clone, Default)]
272pub struct ParquetReaderOptions {
273 pub batch_size: Option<usize>,
275}
276
277#[derive(Debug, Clone, Default)]
279pub struct CsvReaderOptions {
280 pub delimiter: Option<char>,
282 pub has_header: Option<bool>,
284}
285
286#[derive(Debug, Clone, Default)]
288pub struct ParquetWriterOptions {
289 pub compression: Option<String>,
291 pub max_row_group_size: Option<usize>,
293}
294
295#[derive(Debug, Clone, Default)]
297pub struct CsvWriterOptions {
298 pub delimiter: Option<char>,
300 pub has_header: Option<bool>,
302}
303
304#[non_exhaustive]
306#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
307pub enum SqlError {
308 #[error("SQL query is empty")]
310 EmptyQuery,
311 #[error("table name is empty")]
313 EmptyTableName,
314 #[error("unsupported SQL feature: {feature}")]
316 Unsupported { feature: String },
317 #[error("invalid table function: {message}")]
319 InvalidTableFunction { message: String },
320 #[error("DataFusion error: {message}")]
322 DataFusion { message: String },
323 #[error(transparent)]
325 Optimizer(#[from] krishiv_plan::optimizer::OptimizerError),
326 #[error("access denied: {reason}")]
328 AccessDenied { reason: String },
329 #[error("operation {operation_id} was cancelled")]
331 OperationCancelled { operation_id: u64 },
332 #[error("query timed out after {timeout_ms} ms")]
334 Timeout { timeout_ms: u64 },
335}
336
337impl From<datafusion::error::DataFusionError> for SqlError {
338 fn from(value: datafusion::error::DataFusionError) -> Self {
339 Self::DataFusion {
340 message: value.to_string(),
341 }
342 }
343}
344
345#[derive(Debug, Clone, PartialEq, Eq)]
347pub struct SqlPlan {
348 query: String,
349 logical_plan: LogicalPlan,
350}
351
352impl SqlPlan {
353 pub fn query(&self) -> &str {
355 &self.query
356 }
357
358 pub fn logical_plan(&self) -> &LogicalPlan {
360 &self.logical_plan
361 }
362}
363
364const PLAN_CACHE_MAX_ENTRIES: usize = 256;
366
367fn resolve_plan_cache_max_entries() -> usize {
368 std::env::var("KRISHIV_PLAN_CACHE_MAX_ENTRIES")
369 .ok()
370 .and_then(|v| v.parse().ok())
371 .filter(|&n| n > 0)
372 .unwrap_or(PLAN_CACHE_MAX_ENTRIES)
373}
374const STREAMING_CEP_MAX_ROWS_DEFAULT: usize = 100_000;
375
376pub fn resolve_streaming_match_recognize_limit(raw: Option<&str>) -> usize {
380 raw.and_then(|s| s.parse::<usize>().ok())
381 .filter(|n| *n > 0)
382 .unwrap_or(STREAMING_CEP_MAX_ROWS_DEFAULT)
383}
384
385pub fn streaming_match_recognize_limit_from_env() -> usize {
388 resolve_streaming_match_recognize_limit(
389 std::env::var("KRISHIV_MATCH_RECOGNIZE_STREAMING_LIMIT")
390 .ok()
391 .as_deref(),
392 )
393}
394
395pub fn resolve_query_memory_limit_bytes(raw: Option<&str>) -> Option<usize> {
399 raw.and_then(|s| s.trim().parse::<usize>().ok())
400 .filter(|n| *n > 0)
401}
402
403pub fn query_memory_limit_from_env() -> Option<usize> {
429 match std::env::var("KRISHIV_QUERY_MEMORY_LIMIT_BYTES").ok() {
430 Some(raw) => resolve_query_memory_limit_bytes(Some(&raw)),
433 None => cgroup_memory_limit_bytes()
434 .map(|limit| (limit / 4) as usize)
435 .filter(|&n| n > 0),
436 }
437}
438
439pub use krishiv_common::cgroup_memory_limit_bytes;
440
441pub use datafusion::execution::memory_pool::MemoryPool;
445
446pub use datafusion::execution::memory_pool::{MemoryConsumer, MemoryReservation};
453
454pub use datafusion::physical_plan::RecordBatchStream;
463
464pub fn process_query_pool() -> Option<&'static Arc<dyn MemoryPool>> {
475 static POOL: std::sync::LazyLock<Option<Arc<dyn MemoryPool>>> =
476 std::sync::LazyLock::new(|| {
477 let capacity = krishiv_common::ExecutorCapacity::detect();
478 let bytes = capacity.query_pool_bytes?;
479 tracing::info!(
480 capacity = %capacity.summary(),
481 "query memory: one shared FairSpillPool for this process"
482 );
483 Some(EngineMemory::shared_pool(
484 usize::try_from(bytes).unwrap_or(usize::MAX),
485 ))
486 });
487 POOL.as_ref()
488}
489
490fn guarded_fair_pool(bytes: usize) -> Arc<dyn MemoryPool> {
505 let inner: Arc<dyn MemoryPool> =
506 Arc::new(datafusion::execution::memory_pool::FairSpillPool::new(bytes));
507 Arc::new(crate::unspillable_headroom::UnspillableHeadroomPool::new(
508 inner,
509 bytes,
510 crate::unspillable_headroom::headroom_bytes(bytes),
511 ))
512}
513
514fn process_query_pool_fair_share_bytes() -> usize {
518 krishiv_common::ExecutorCapacity::detect()
519 .min_task_memory_share_bytes()
520 .map_or(usize::MAX, |bytes| {
521 usize::try_from(bytes).unwrap_or(usize::MAX)
522 })
523}
524
525#[derive(Clone)]
537pub enum EngineMemory {
538 Unbounded,
540 Private(usize),
543 Shared {
549 pool: Arc<dyn datafusion::execution::memory_pool::MemoryPool>,
551 fair_share_bytes: usize,
553 },
554}
555
556impl EngineMemory {
557 #[must_use]
560 pub fn from_limit(bytes: Option<usize>) -> Self {
561 bytes.map_or(Self::Unbounded, Self::Private)
562 }
563
564 #[must_use]
570 pub fn shared_pool(bytes: usize) -> Arc<dyn MemoryPool> {
571 guarded_fair_pool(bytes)
572 }
573
574 #[must_use]
586 pub fn for_this_process() -> Self {
587 match process_query_pool() {
588 Some(pool) => Self::Shared {
589 pool: Arc::clone(pool),
590 fair_share_bytes: process_query_pool_fair_share_bytes(),
591 },
592 None => Self::Unbounded,
593 }
594 }
595
596 #[must_use]
599 pub fn sizing_bytes(&self) -> Option<usize> {
600 match self {
601 Self::Unbounded => None,
602 Self::Private(bytes) => Some(*bytes),
603 Self::Shared {
604 fair_share_bytes, ..
605 } => Some(*fair_share_bytes),
606 }
607 }
608
609 fn pool(&self) -> Option<Arc<dyn datafusion::execution::memory_pool::MemoryPool>> {
611 match self {
612 Self::Unbounded => None,
613 Self::Private(bytes) => Some(guarded_fair_pool(*bytes)),
619 Self::Shared { pool, .. } => Some(Arc::clone(pool)),
620 }
621 }
622}
623
624impl fmt::Debug for EngineMemory {
625 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
626 match self {
627 Self::Unbounded => f.write_str("EngineMemory::Unbounded"),
628 Self::Private(bytes) => write!(f, "EngineMemory::Private({bytes})"),
629 Self::Shared {
630 fair_share_bytes, ..
631 } => write!(f, "EngineMemory::Shared(share={fair_share_bytes})"),
632 }
633 }
634}
635
636static RUNTIME_FILTERS_OVERRIDE: std::sync::atomic::AtomicU8 =
641 std::sync::atomic::AtomicU8::new(u8::MAX);
642
643#[doc(hidden)]
646pub fn set_runtime_filters_for_tests(enabled: bool) {
647 RUNTIME_FILTERS_OVERRIDE.store(u8::from(enabled), std::sync::atomic::Ordering::Relaxed);
648}
649
650pub fn runtime_filters_enabled_from_env() -> bool {
655 match RUNTIME_FILTERS_OVERRIDE.load(std::sync::atomic::Ordering::Relaxed) {
656 0 => return false,
657 1 => return true,
658 _ => {}
659 }
660 !matches!(
661 std::env::var("KRISHIV_RUNTIME_FILTERS")
662 .unwrap_or_default()
663 .trim()
664 .to_ascii_lowercase()
665 .as_str(),
666 "off" | "0" | "false" | "disabled"
667 )
668}
669
670pub fn batch_size_from_env() -> usize {
674 std::env::var("KRISHIV_BATCH_SIZE")
675 .ok()
676 .and_then(|v| v.parse::<usize>().ok())
677 .filter(|n| *n > 0)
678 .unwrap_or(8192)
679}
680
681pub fn default_parallelism_from_env() -> NonZeroUsize {
685 std::env::var("KRISHIV_TARGET_PARALLELISM")
686 .ok()
687 .and_then(|v| v.parse::<usize>().ok())
688 .and_then(NonZeroUsize::new)
689 .unwrap_or_else(|| std::thread::available_parallelism().unwrap_or(NonZeroUsize::MIN))
690}
691
692const DEFAULT_SORT_SPILL_RESERVATION_BYTES: usize = 10 * 1024 * 1024;
698
699const MIN_SORT_SPILL_RESERVATION_BYTES: usize = 64 * 1024;
702
703#[must_use]
732pub fn with_krishiv_optimizer_rules(
733 builder: datafusion::execution::session_state::SessionStateBuilder,
734) -> datafusion::execution::session_state::SessionStateBuilder {
735 with_krishiv_optimizer_rules_with_join_threshold(builder, None)
736}
737
738#[must_use]
741pub fn with_krishiv_optimizer_rules_with_join_threshold(
742 builder: datafusion::execution::session_state::SessionStateBuilder,
743 spill_join_build_bytes: Option<u64>,
744) -> datafusion::execution::session_state::SessionStateBuilder {
745 let spillable_join = match spill_join_build_bytes {
746 Some(bytes) => crate::spillable_join::SpillableJoinSelection::with_threshold(Some(bytes)),
759 None => crate::spillable_join::SpillableJoinSelection::from_capacity(),
760 }
761 .with_grace_where_plans_are_never_encoded();
769 builder
770 .with_physical_optimizer_rule(std::sync::Arc::new(
771 crate::coop_amplifiers::CooperativeAmplifiers::new(),
772 ))
773 .with_physical_optimizer_rule(std::sync::Arc::new(spillable_join))
779 .with_optimizer_rule(std::sync::Arc::new(
782 crate::semi_join_reduction::SemiJoinReductionThroughAggregate,
783 ))
784 .with_optimizer_rule(std::sync::Arc::new(
793 crate::semi_join_reduction::SemiJoinPushdownThroughInnerJoin::default(),
794 ))
795 .with_optimizer_rule(std::sync::Arc::new(
805 crate::semi_join_reduction::SemiJoinReductionFromSelectiveDimension::default(),
806 ))
807 .with_optimizer_rule(std::sync::Arc::new(
819 crate::late_materialize::LateMaterializeTopKAggregate::default(),
820 ))
821}
822
823pub(crate) fn build_single_node_session_config(
837 target_partitions: NonZeroUsize,
838 memory_limit_bytes: Option<usize>,
839) -> datafusion::prelude::SessionConfig {
840 let tp = target_partitions.get();
841 let batch_size = batch_size_from_env();
842 let mut config = datafusion::prelude::SessionConfig::new()
843 .with_target_partitions(tp)
844 .with_batch_size(batch_size)
845 .with_information_schema(true)
846 .set_bool(
847 "datafusion.optimizer.enable_round_robin_repartition",
848 tp > 1,
849 )
850 .set_bool(
855 "datafusion.optimizer.enable_dynamic_filter_pushdown",
856 runtime_filters_enabled_from_env(),
857 )
858 .set_bool(
859 "datafusion.optimizer.enable_join_dynamic_filter_pushdown",
860 runtime_filters_enabled_from_env(),
861 )
862 .set_bool(
863 "datafusion.optimizer.enable_topk_dynamic_filter_pushdown",
864 runtime_filters_enabled_from_env(),
865 )
866 .set_bool(
867 "datafusion.optimizer.enable_aggregate_dynamic_filter_pushdown",
868 runtime_filters_enabled_from_env(),
869 );
870 config.options_mut().sql_parser.dialect = datafusion::common::config::Dialect::DuckDB;
878 if let Some(limit) = memory_limit_bytes {
885 let scaled = (limit / 4).clamp(
886 MIN_SORT_SPILL_RESERVATION_BYTES,
887 DEFAULT_SORT_SPILL_RESERVATION_BYTES,
888 );
889 config = config.with_sort_spill_reservation_bytes(scaled);
890 }
891 config
892}
893
894#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
898type IcebergCatalogRegistry =
899 Arc<std::sync::RwLock<Vec<(Arc<catalog::unified::KrishivCatalog>, String)>>>;
900
901#[derive(Clone)]
914pub struct SqlEngine {
915 context: SessionContext,
916 target_parallelism: NonZeroUsize,
917 krishiv_catalog: Option<Arc<RwLock<InMemoryCatalog>>>,
918 udf_registry: Option<std::sync::Arc<std::sync::RwLock<krishiv_plan::udf::UdfRegistry>>>,
919 streaming_sources: Arc<RwLock<std::collections::HashSet<String>>>,
922 streaming_registration: Arc<Mutex<()>>,
924 has_streaming_sources: Arc<AtomicBool>,
929 udf_limits: Option<krishiv_plan::udf::ResourceLimits>,
932 udf_registry_version: Arc<AtomicU64>,
936 udf_last_synced_version: Arc<AtomicU64>,
939 plan_cache: Arc<Mutex<PlanCache>>,
945 shuffle_partitions: Arc<std::sync::RwLock<Option<u32>>>,
948 table_row_counts: Arc<std::sync::RwLock<HashMap<String, u64>>>,
953 memory_limit_bytes: Option<usize>,
958 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
962 iceberg_catalogs: IcebergCatalogRegistry,
963 live_table_registry: Arc<live_table::LiveTableRegistry>,
965 incremental_view_registry: Arc<incremental_view::IncrementalViewRegistry>,
967 pipeline_registry: Arc<pipeline_ddl::PipelineRegistry>,
969 operation_registry: Arc<OperationRegistry>,
971}
972
973impl fmt::Debug for SqlEngine {
974 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
975 f.debug_struct("SqlEngine")
976 .field("backend", &"datafusion")
977 .finish_non_exhaustive()
978 }
979}
980
981impl Default for SqlEngine {
982 fn default() -> Self {
983 Self::new()
984 }
985}
986
987impl SqlEngine {
988 pub fn new() -> Self {
1002 Self::new_with_engine_memory(EngineMemory::for_this_process())
1003 }
1004
1005 pub fn new_with_memory_limit(memory_limit_bytes: Option<usize>) -> Self {
1017 Self::new_with_engine_memory(EngineMemory::from_limit(memory_limit_bytes))
1018 }
1019
1020 pub fn new_with_engine_memory(engine_memory: EngineMemory) -> Self {
1030 let parallelism = default_parallelism_from_env();
1031 match Self::build_local(
1032 None,
1033 WindowFnRegistration::Register,
1034 parallelism,
1035 engine_memory.clone(),
1036 ) {
1037 Ok(engine) => engine,
1038 Err(err) => {
1039 tracing::warn!(
1040 error = %err,
1041 "SqlEngine::new: window helper UDF registration failed; \
1042 window SQL functions will be unavailable, other queries are unaffected"
1043 );
1044 Self::build_local(
1045 None,
1046 WindowFnRegistration::Skip,
1047 parallelism,
1048 engine_memory.clone(),
1049 )
1050 .unwrap_or_else(|err| {
1051 tracing::error!(
1052 error = %err,
1053 "memory-limited DataFusion runtime construction failed; \
1054 falling back to an unbounded engine"
1055 );
1056 Self::build_local(
1057 None,
1058 WindowFnRegistration::Skip,
1059 parallelism,
1060 EngineMemory::Unbounded,
1061 )
1062 .unwrap_or_else(|_| Self::build_absolute_minimal(parallelism))
1063 })
1064 }
1065 }
1066 }
1067
1068 pub fn try_new() -> SqlResult<Self> {
1073 Self::build_local(
1074 None,
1075 WindowFnRegistration::Register,
1076 default_parallelism_from_env(),
1077 EngineMemory::for_this_process(),
1078 )
1079 }
1080
1081 pub fn with_in_memory_catalog(catalog: Arc<RwLock<InMemoryCatalog>>) -> SqlResult<Self> {
1083 if krishiv_common::profile_requires_fail_closed_metadata(
1084 krishiv_common::resolve_durability_profile(),
1085 ) {
1086 return Err(SqlError::DataFusion {
1087 message: String::from(
1088 "InMemoryCatalog is dev-only; configure a durable REST or file-backed \
1089 catalog for production deployments",
1090 ),
1091 });
1092 }
1093 Self::build_local(
1094 Some(catalog),
1095 WindowFnRegistration::Register,
1096 default_parallelism_from_env(),
1097 EngineMemory::for_this_process(),
1098 )
1099 }
1100
1101 #[must_use]
1112 pub fn with_target_parallelism(mut self, n: NonZeroUsize) -> Self {
1113 self.target_parallelism = n;
1114 self.apply_target_partitions(n);
1115 self
1116 }
1117
1118 fn apply_target_partitions(&self, n: NonZeroUsize) {
1127 let state_ref = self.context.state_ref();
1128 let mut state = state_ref.write();
1129 let options = state.config_mut().options_mut();
1130 options.execution.target_partitions = n.get();
1131 options.optimizer.enable_round_robin_repartition = n.get() > 1;
1132 }
1133
1134 pub fn target_parallelism(&self) -> NonZeroUsize {
1136 self.target_parallelism
1137 }
1138
1139 pub fn memory_limit_bytes(&self) -> Option<usize> {
1141 self.memory_limit_bytes
1142 }
1143
1144 pub fn session_context(&self) -> &SessionContext {
1150 &self.context
1151 }
1152
1153 pub fn shuffle_partitions(&self) -> Option<u32> {
1155 *self
1156 .shuffle_partitions
1157 .read()
1158 .unwrap_or_else(|e| e.into_inner())
1159 }
1160
1161 pub fn table_row_counts(&self) -> Arc<std::sync::RwLock<HashMap<String, u64>>> {
1167 Arc::clone(&self.table_row_counts)
1168 }
1169
1170 pub fn registered_table_names(&self) -> Vec<String> {
1176 let mut names = Vec::new();
1177 for catalog_name in self.context.catalog_names() {
1178 let Some(catalog) = self.context.catalog(&catalog_name) else {
1179 continue;
1180 };
1181 for schema_name in catalog.schema_names() {
1182 let Some(schema) = catalog.schema(&schema_name) else {
1183 continue;
1184 };
1185 names.extend(schema.table_names());
1186 }
1187 }
1188 names.sort();
1189 names.dedup();
1190 names
1191 }
1192
1193 fn make_sql_df(&self, name: &str, dataframe: DataFusionDataFrame) -> SqlDataFrame {
1196 SqlDataFrame::new(name, dataframe, self.table_row_counts())
1197 .with_context(self.context.clone())
1198 }
1199
1200 fn attach_query_metadata(&self, df: SqlDataFrame, query: &str) -> SqlDataFrame {
1202 let kind = if self.is_streaming_query(query).unwrap_or(false) {
1203 ExecutionKind::Streaming
1204 } else {
1205 ExecutionKind::Batch
1206 };
1207 df.with_query(query).with_execution_kind(kind)
1208 }
1209
1210 #[must_use]
1215 pub fn with_shuffle_partitions(self, n: Option<u32>) -> Self {
1216 if let Ok(mut guard) = self.shuffle_partitions.write() {
1217 *guard = n;
1218 }
1219 self
1220 }
1221
1222 fn build_local(
1232 krishiv_catalog: Option<Arc<RwLock<InMemoryCatalog>>>,
1233 window_fn_registration: WindowFnRegistration,
1234 target_partitions: NonZeroUsize,
1235 engine_memory: EngineMemory,
1236 ) -> SqlResult<Self> {
1237 let memory_limit_bytes = engine_memory.sizing_bytes();
1238 let streaming_sources: Arc<RwLock<std::collections::HashSet<String>>> =
1242 Arc::new(RwLock::new(std::collections::HashSet::new()));
1243
1244 let mut state_builder = with_krishiv_optimizer_rules(
1245 datafusion::execution::session_state::SessionStateBuilder::new()
1246 .with_default_features(),
1247 )
1248 .with_config(build_single_node_session_config(
1249 target_partitions,
1250 memory_limit_bytes,
1251 ));
1252 {
1253 let mut runtime_builder = datafusion::execution::runtime_env::RuntimeEnvBuilder::new()
1259 .with_object_store_registry(Arc::new(
1260 crate::object_store_registry::LazyCloudObjectStoreRegistry::new(),
1261 ));
1262 if let Some(pool) = engine_memory.pool() {
1263 runtime_builder = runtime_builder.with_memory_pool(pool);
1271 }
1272 let runtime_env = runtime_builder
1273 .build_arc()
1274 .map_err(|e| SqlError::DataFusion {
1275 message: format!(
1276 "failed to build DataFusion runtime \
1277 (memory limit {memory_limit_bytes:?} bytes): {e}"
1278 ),
1279 })?;
1280 state_builder = state_builder.with_runtime_env(runtime_env);
1281 }
1282 let mut state = state_builder.build();
1283 crate::connector_table::register_connector_table_factories(
1287 state.table_factories_mut(),
1288 streaming_sources.clone(),
1289 );
1290 let context = SessionContext::new_with_state(state);
1291 if let Some(catalog) = &krishiv_catalog {
1292 context.register_catalog(
1293 "krishiv",
1294 Arc::new(DataFusionCatalogBridge::new(catalog.clone())),
1295 );
1296 }
1297 if matches!(window_fn_registration, WindowFnRegistration::Register) {
1298 window_functions::register_window_functions(&context).map_err(|e| {
1299 SqlError::DataFusion {
1300 message: format!("failed to register window helper UDFs: {e}"),
1301 }
1302 })?;
1303 }
1304 json_functions::register_json_functions(&context).map_err(|e| SqlError::DataFusion {
1307 message: format!("failed to register JSON UDFs: {e}"),
1308 })?;
1309 higher_order_functions::register_higher_order_spark_functions(&context).map_err(|e| {
1312 SqlError::DataFusion {
1313 message: format!("failed to register higher-order UDFs: {e}"),
1314 }
1315 })?;
1316 spark_functions::register_spark_scalar_functions(&context).map_err(|e| {
1318 SqlError::DataFusion {
1319 message: format!("failed to register Spark scalar UDFs: {e}"),
1320 }
1321 })?;
1322 Ok(Self {
1323 context,
1324 target_parallelism: target_partitions,
1325 krishiv_catalog,
1326 udf_registry: None,
1327 streaming_sources,
1328 streaming_registration: Arc::new(Mutex::new(())),
1329 has_streaming_sources: Arc::new(AtomicBool::new(false)),
1330 udf_limits: None,
1331 udf_registry_version: Arc::new(AtomicU64::new(0)),
1332 udf_last_synced_version: Arc::new(AtomicU64::new(u64::MAX)),
1333 plan_cache: Arc::new(Mutex::new(PlanCache::new(resolve_plan_cache_max_entries()))),
1334 shuffle_partitions: Arc::new(std::sync::RwLock::new(None)),
1335 table_row_counts: Arc::new(std::sync::RwLock::new(HashMap::new())),
1336 memory_limit_bytes,
1337 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
1338 iceberg_catalogs: Arc::new(std::sync::RwLock::new(Vec::new())),
1339 live_table_registry: Arc::new(live_table::LiveTableRegistry::new()),
1340 incremental_view_registry: Arc::new(incremental_view::IncrementalViewRegistry::new()),
1341 pipeline_registry: Arc::new(pipeline_ddl::PipelineRegistry::new()),
1342 operation_registry: Arc::new(OperationRegistry::new()),
1343 })
1344 }
1345
1346 fn build_absolute_minimal(target_partitions: NonZeroUsize) -> Self {
1350 let streaming_sources: Arc<RwLock<std::collections::HashSet<String>>> =
1351 Arc::new(RwLock::new(std::collections::HashSet::new()));
1352 let mut state = with_krishiv_optimizer_rules(
1353 datafusion::execution::session_state::SessionStateBuilder::new()
1354 .with_default_features(),
1355 )
1356 .with_config(build_single_node_session_config(target_partitions, None))
1357 .build();
1358 crate::connector_table::register_connector_table_factories(
1359 state.table_factories_mut(),
1360 streaming_sources.clone(),
1361 );
1362 let context = SessionContext::new_with_state(state);
1363 Self {
1364 context,
1365 target_parallelism: target_partitions,
1366 krishiv_catalog: None,
1367 udf_registry: None,
1368 streaming_sources,
1369 streaming_registration: Arc::new(Mutex::new(())),
1370 has_streaming_sources: Arc::new(AtomicBool::new(false)),
1371 udf_limits: None,
1372 udf_registry_version: Arc::new(AtomicU64::new(0)),
1373 udf_last_synced_version: Arc::new(AtomicU64::new(u64::MAX)),
1374 plan_cache: Arc::new(Mutex::new(PlanCache::new(resolve_plan_cache_max_entries()))),
1375 shuffle_partitions: Arc::new(std::sync::RwLock::new(None)),
1376 table_row_counts: Arc::new(std::sync::RwLock::new(HashMap::new())),
1377 memory_limit_bytes: None,
1378 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
1379 iceberg_catalogs: Arc::new(std::sync::RwLock::new(Vec::new())),
1380 live_table_registry: Arc::new(live_table::LiveTableRegistry::new()),
1381 incremental_view_registry: Arc::new(incremental_view::IncrementalViewRegistry::new()),
1382 pipeline_registry: Arc::new(pipeline_ddl::PipelineRegistry::new()),
1383 operation_registry: Arc::new(OperationRegistry::new()),
1384 }
1385 }
1386
1387 pub fn register_streaming_table(
1398 &self,
1399 name: &str,
1400 schema: arrow::datatypes::SchemaRef,
1401 ) -> SqlResult<Arc<ContinuousTableInput>> {
1402 let _registration = self.lock_streaming_registration()?;
1403 self.validate_new_streaming_table(name, &schema)?;
1404 let (table, input) = crate::streaming::create_continuous_table(schema).map_err(|e| {
1405 SqlError::DataFusion {
1406 message: e.to_string(),
1407 }
1408 })?;
1409 self.register_new_streaming_provider(name, table)?;
1410 self.streaming_sources
1411 .write()
1412 .unwrap_or_else(|e| e.into_inner())
1413 .insert(name.to_string());
1414 self.has_streaming_sources.store(true, Ordering::Release);
1415 self.invalidate_plan_cache();
1416 Ok(input)
1417 }
1418
1419 pub fn register_streaming_table_with_capacity(
1424 &self,
1425 name: &str,
1426 schema: arrow::datatypes::SchemaRef,
1427 capacity: usize,
1428 ) -> SqlResult<Arc<ContinuousTableInput>> {
1429 let _registration = self.lock_streaming_registration()?;
1430 self.validate_new_streaming_table(name, &schema)?;
1431 let (table, input) = crate::streaming::create_continuous_table_with_capacity(
1432 schema, capacity,
1433 )
1434 .map_err(|e| SqlError::DataFusion {
1435 message: e.to_string(),
1436 })?;
1437 self.register_new_streaming_provider(name, table)?;
1438 self.streaming_sources
1439 .write()
1440 .unwrap_or_else(|e| e.into_inner())
1441 .insert(name.to_string());
1442 self.has_streaming_sources.store(true, Ordering::Release);
1443 self.invalidate_plan_cache();
1444 Ok(input)
1445 }
1446
1447 fn lock_streaming_registration(&self) -> SqlResult<std::sync::MutexGuard<'_, ()>> {
1448 self.streaming_registration
1449 .lock()
1450 .map_err(|error| SqlError::DataFusion {
1451 message: format!("streaming table registration lock poisoned: {error}"),
1452 })
1453 }
1454
1455 fn validate_new_streaming_table(
1456 &self,
1457 name: &str,
1458 schema: &arrow::datatypes::SchemaRef,
1459 ) -> SqlResult<()> {
1460 if name.trim().is_empty() {
1461 return Err(SqlError::EmptyTableName);
1462 }
1463 if schema.fields().is_empty() {
1464 return Err(SqlError::DataFusion {
1465 message: "streaming table schema must contain at least one field".into(),
1466 });
1467 }
1468 if self
1469 .context
1470 .table_exist(name)
1471 .map_err(|error| SqlError::DataFusion {
1472 message: error.to_string(),
1473 })?
1474 {
1475 return Err(SqlError::DataFusion {
1476 message: format!("table '{name}' is already registered"),
1477 });
1478 }
1479 Ok(())
1480 }
1481
1482 fn register_new_streaming_provider(
1483 &self,
1484 name: &str,
1485 table: Arc<dyn datafusion::catalog::TableProvider>,
1486 ) -> SqlResult<()> {
1487 let previous =
1488 self.context
1489 .register_table(name, table)
1490 .map_err(|error| SqlError::DataFusion {
1491 message: error.to_string(),
1492 })?;
1493 if let Some(previous) = previous {
1494 self.context
1495 .register_table(name, previous)
1496 .map_err(|error| SqlError::DataFusion {
1497 message: format!(
1498 "table '{name}' was concurrently registered and could not be restored: \
1499 {error}"
1500 ),
1501 })?;
1502 return Err(SqlError::DataFusion {
1503 message: format!("table '{name}' was concurrently registered"),
1504 });
1505 }
1506 Ok(())
1507 }
1508
1509 pub fn register_kafka_source(
1523 &self,
1524 table_name: impl AsRef<str>,
1525 schema: arrow::datatypes::SchemaRef,
1526 bootstrap_servers: impl Into<String>,
1527 topic: impl Into<String>,
1528 group_id: impl Into<String>,
1529 ) -> SqlResult<()> {
1530 let table_name = table_name.as_ref();
1531 if table_name.trim().is_empty() {
1532 return Err(SqlError::EmptyTableName);
1533 }
1534 let config = krishiv_connectors::kafka::KafkaConfig {
1535 bootstrap_servers: bootstrap_servers.into(),
1536 topic: topic.into(),
1537 group_id: group_id.into(),
1538 auto_commit_interval_ms: {
1539 let profile = krishiv_common::resolve_durability_profile();
1540 if krishiv_common::requires_manual_kafka_commit(profile) {
1541 None
1542 } else {
1543 Some(1_000)
1544 }
1545 },
1546 security_protocol: None,
1547 ssl_ca_location: None,
1548 ssl_certificate_location: None,
1549 ssl_key_location: None,
1550 ssl_key_password: None,
1551 sasl_username: None,
1552 sasl_password: None,
1553 sasl_mechanisms: None,
1554 enable_idempotence: None,
1555 transactional_id: None,
1556 };
1557 let table =
1558 crate::kafka_table::create_kafka_streaming_table(schema, config).map_err(|e| {
1559 SqlError::DataFusion {
1560 message: e.to_string(),
1561 }
1562 })?;
1563 if self
1564 .context
1565 .table_exist(table_name)
1566 .map_err(SqlError::from)?
1567 {
1568 let _ = self
1569 .context
1570 .deregister_table(table_name)
1571 .map_err(SqlError::from)?;
1572 }
1573 self.context
1574 .register_table(table_name, table)
1575 .map_err(|e| SqlError::DataFusion {
1576 message: e.to_string(),
1577 })?;
1578 self.streaming_sources
1579 .write()
1580 .unwrap_or_else(|e| e.into_inner())
1581 .insert(table_name.to_string());
1582 self.has_streaming_sources.store(true, Ordering::Release);
1583 self.invalidate_plan_cache();
1584 Ok(())
1585 }
1586
1587 pub async fn sql_to_kafka(
1597 &self,
1598 sql: impl AsRef<str>,
1599 bootstrap_servers: impl Into<String>,
1600 topic: impl Into<String>,
1601 ) -> SqlResult<u64> {
1602 use futures::StreamExt;
1603 use krishiv_connectors::Sink as _;
1604 use krishiv_connectors::kafka::{KafkaConfig, KafkaSink};
1605
1606 let config = KafkaConfig {
1607 bootstrap_servers: bootstrap_servers.into(),
1608 topic: topic.into(),
1609 group_id: "krishiv-sql-writer".into(),
1610 auto_commit_interval_ms: None,
1611 security_protocol: None,
1612 ssl_ca_location: None,
1613 ssl_certificate_location: None,
1614 ssl_key_location: None,
1615 ssl_key_password: None,
1616 sasl_username: None,
1617 sasl_password: None,
1618 sasl_mechanisms: None,
1619 enable_idempotence: None,
1620 transactional_id: None,
1621 };
1622 let mut sink = KafkaSink::new(config).map_err(|e| SqlError::DataFusion {
1623 message: e.to_string(),
1624 })?;
1625
1626 let df = self.sql(sql.as_ref()).await?;
1627 let mut stream = df.execute_stream().await?;
1628 let mut total_rows = 0u64;
1629
1630 while let Some(result) = stream.next().await {
1631 let batch = result.map_err(|e| SqlError::DataFusion {
1632 message: e.to_string(),
1633 })?;
1634 if batch.num_rows() > 0 {
1635 total_rows += batch.num_rows() as u64;
1636 sink.write_batch(batch)
1637 .await
1638 .map_err(|e| SqlError::DataFusion {
1639 message: e.to_string(),
1640 })?;
1641 }
1642 }
1643 sink.flush().await.map_err(|e| SqlError::DataFusion {
1644 message: e.to_string(),
1645 })?;
1646 Ok(total_rows)
1647 }
1648
1649 pub fn with_udf_limits(mut self, limits: krishiv_plan::udf::ResourceLimits) -> Self {
1653 self.udf_limits = Some(limits);
1654 self
1655 }
1656
1657 pub fn is_streaming_source(&self, table_name: &str) -> bool {
1659 self.streaming_sources
1660 .read()
1661 .unwrap_or_else(|e| e.into_inner())
1662 .contains(table_name)
1663 }
1664
1665 pub fn register_streaming_source_name(&self, table_name: impl Into<String>) -> SqlResult<()> {
1674 let name: String = table_name.into();
1675 if name.trim().is_empty() {
1676 return Err(SqlError::EmptyTableName);
1677 }
1678 self.streaming_sources
1679 .write()
1680 .unwrap_or_else(|e| e.into_inner())
1681 .insert(name);
1682 self.has_streaming_sources.store(true, Ordering::Release);
1683 self.invalidate_plan_cache();
1684 Ok(())
1685 }
1686
1687 pub fn deregister_streaming_source(&self, name: &str) -> SqlResult<()> {
1693 if name.trim().is_empty() {
1694 return Err(SqlError::EmptyTableName);
1695 }
1696 let _ = self
1698 .context
1699 .deregister_table(name)
1700 .map_err(SqlError::from)?;
1701 {
1702 let mut sources = self
1703 .streaming_sources
1704 .write()
1705 .unwrap_or_else(|e| e.into_inner());
1706 sources.remove(name);
1707 if sources.is_empty() {
1708 self.has_streaming_sources.store(false, Ordering::Release);
1709 }
1710 self.invalidate_plan_cache();
1714 }
1715 Ok(())
1716 }
1717
1718 pub fn live_table_registry(&self) -> &Arc<live_table::LiveTableRegistry> {
1720 &self.live_table_registry
1721 }
1722
1723 pub fn incremental_view_registry(&self) -> &Arc<incremental_view::IncrementalViewRegistry> {
1725 &self.incremental_view_registry
1726 }
1727
1728 pub fn pipeline_registry(&self) -> &Arc<pipeline_ddl::PipelineRegistry> {
1730 &self.pipeline_registry
1731 }
1732
1733 pub fn operation_registry(&self) -> &Arc<OperationRegistry> {
1735 &self.operation_registry
1736 }
1737
1738 pub fn deregister_table(&self, name: &str) -> SqlResult<()> {
1757 if name.trim().is_empty() {
1758 return Err(SqlError::EmptyTableName);
1759 }
1760 let _ = self
1761 .context
1762 .deregister_table(name)
1763 .map_err(SqlError::from)?;
1764 {
1765 let mut sources = self
1766 .streaming_sources
1767 .write()
1768 .unwrap_or_else(|e| e.into_inner());
1769 sources.remove(name);
1770 if sources.is_empty() {
1771 self.has_streaming_sources.store(false, Ordering::Release);
1772 }
1773 self.invalidate_plan_cache();
1778 }
1779 Ok(())
1780 }
1781
1782 pub fn register_table_udf_fn(
1806 &self,
1807 name: impl Into<String>,
1808 schema: arrow::datatypes::Schema,
1809 f: impl Fn(
1810 &[krishiv_plan::udf::ScalarValue],
1811 ) -> Result<arrow::record_batch::RecordBatch, krishiv_plan::udf::UdfError>
1812 + Send
1813 + Sync
1814 + 'static,
1815 ) -> SqlResult<()> {
1816 let udf =
1817 create_function_ddl::ClosureTableUdf::try_new(name, schema, std::sync::Arc::new(f))
1818 .map_err(|error| SqlError::InvalidTableFunction {
1819 message: error.to_string(),
1820 })?;
1821 if let Some(registry) = &self.udf_registry {
1822 let mut guard = registry.write().map_err(|e| SqlError::DataFusion {
1823 message: e.to_string(),
1824 })?;
1825 guard.register_table(std::sync::Arc::new(udf.clone()));
1826 }
1827 udf::register_single_table_udf(&self.context, std::sync::Arc::new(udf))
1828 .map_err(SqlError::from)?;
1829 self.bump_udf_version();
1830 Ok(())
1831 }
1832
1833 pub fn is_streaming_query(&self, sql: &str) -> SqlResult<bool> {
1835 if !self.has_streaming_sources.load(Ordering::Acquire) {
1838 return Ok(false);
1839 }
1840 let sources = self
1841 .streaming_sources
1842 .read()
1843 .unwrap_or_else(|e| e.into_inner());
1844 if sources.is_empty() {
1845 return Ok(false);
1846 }
1847 let dialect = GenericDialect {};
1848 let statements = Parser::parse_sql(&dialect, sql).map_err(|e| SqlError::DataFusion {
1849 message: e.to_string(),
1850 })?;
1851 for stmt in &statements {
1852 let mut is_streaming = false;
1853 let _ = visit_relations(stmt, |relation| {
1854 let full = relation.to_string();
1857 let table_name = full.split('.').next_back().unwrap_or(&full);
1858 if sources.contains(table_name) {
1859 is_streaming = true;
1860 return ControlFlow::Break(());
1861 }
1862 ControlFlow::Continue(())
1863 });
1864 if is_streaming {
1865 return Ok(true);
1866 }
1867 }
1868 Ok(false)
1869 }
1870
1871 pub fn krishiv_catalog(&self) -> Option<&Arc<RwLock<InMemoryCatalog>>> {
1873 self.krishiv_catalog.as_ref()
1874 }
1875
1876 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
1885 #[must_use]
1886 pub fn with_iceberg_catalog(
1887 self,
1888 catalog: std::sync::Arc<catalog::unified::KrishivCatalog>,
1889 catalog_name: impl Into<String>,
1890 ) -> Self {
1891 self.register_iceberg_catalog(catalog, catalog_name);
1892 self
1893 }
1894
1895 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
1902 pub fn register_iceberg_catalog(
1903 &self,
1904 catalog: std::sync::Arc<catalog::unified::KrishivCatalog>,
1905 catalog_name: impl Into<String>,
1906 ) {
1907 let catalog_name = catalog_name.into();
1908 let bridge = catalog::iceberg_catalog_bridge::IcebergCatalogBridge::new(
1909 Arc::clone(&catalog),
1910 catalog_name.clone(),
1911 );
1912 self.context
1913 .register_catalog(catalog_name.clone(), Arc::new(bridge));
1914 self.iceberg_catalogs
1915 .write()
1916 .unwrap_or_else(|e| e.into_inner())
1917 .push((catalog, catalog_name));
1918 self.invalidate_plan_cache();
1919 }
1920
1921 pub async fn register_iceberg_rest_catalog_from_env(&self) -> Result<bool, String> {
1933 #[cfg(feature = "rest-catalog")]
1934 {
1935 let uri = match std::env::var("KRISHIV_ICEBERG_REST_URI") {
1936 Ok(uri) => uri,
1937 Err(_) => return Ok(false),
1938 };
1939 let warehouse = std::env::var("KRISHIV_ICEBERG_REST_WAREHOUSE").unwrap_or_default();
1940 let token = std::env::var("KRISHIV_ICEBERG_REST_TOKEN").ok();
1941 let name =
1946 std::env::var("KRISHIV_ICEBERG_REST_NAME").unwrap_or_else(|_| String::from("main"));
1947 self.register_s3_object_store_for_warehouse(&warehouse)?;
1953 let catalog = std::sync::Arc::new(
1954 catalog::unified::KrishivCatalog::rest(&uri, &warehouse, token.as_deref())
1955 .await
1956 .map_err(|e| format!("iceberg REST catalog at {uri}: {e}"))?,
1957 );
1958 self.register_iceberg_catalog(std::sync::Arc::clone(&catalog), &name);
1959 if name != "krishiv" {
1965 self.register_iceberg_catalog(catalog, "krishiv");
1966 }
1967 Ok(true)
1968 }
1969 #[cfg(not(feature = "rest-catalog"))]
1970 {
1971 Ok(false)
1972 }
1973 }
1974
1975 #[must_use]
1977 pub fn with_udf_registry(
1978 mut self,
1979 registry: std::sync::Arc<std::sync::RwLock<krishiv_plan::udf::UdfRegistry>>,
1980 ) -> Self {
1981 self.udf_registry = Some(registry);
1982 self.bump_udf_version();
1984 self
1985 }
1986
1987 pub(crate) fn bump_udf_version(&self) {
2005 self.udf_registry_version.fetch_add(1, Ordering::Release);
2006 self.invalidate_plan_cache();
2007 }
2008
2009 fn invalidate_plan_cache(&self) {
2014 match self.plan_cache.lock() {
2015 Ok(mut cache) => cache.clear(),
2016 Err(poisoned) => poisoned.into_inner().clear(),
2017 }
2018 }
2019
2020 pub fn clear_plan_cache(&self) {
2023 self.invalidate_plan_cache();
2024 }
2025
2026 pub async fn register_python_udfs_from_sql(&self, sql: &str) -> SqlResult<String> {
2035 const SCALAR_PREFIX: &str = "/* krishiv-register-python-udf:";
2036 const AGG_PREFIX: &str = "/* krishiv-register-python-udaf:";
2037 if !sql.contains(SCALAR_PREFIX) && !sql.contains(AGG_PREFIX) {
2038 return Ok(sql.to_string());
2039 }
2040 let mut out = String::with_capacity(sql.len());
2041 let mut rest = sql;
2042 loop {
2043 let agg = rest.find(AGG_PREFIX).map(|i| (i, true, AGG_PREFIX.len()));
2046 let scalar = rest
2047 .find(SCALAR_PREFIX)
2048 .map(|i| (i, false, SCALAR_PREFIX.len()));
2049 let next = match (agg, scalar) {
2050 (Some(a), Some(s)) => Some(if a.0 <= s.0 { a } else { s }),
2051 (Some(a), None) => Some(a),
2052 (None, Some(s)) => Some(s),
2053 (None, None) => None,
2054 };
2055 let Some((start, is_aggregate, prefix_len)) = next else {
2056 break;
2057 };
2058 out.push_str(&rest[..start]);
2059 let after = &rest[start + prefix_len..];
2060 let Some(end) = after.find(" */") else {
2061 out.push_str(&rest[start..]);
2063 return Ok(out);
2064 };
2065 self.register_python_udf_directive(&after[..end], is_aggregate)
2066 .await?;
2067 rest = &after[end + " */".len()..];
2068 }
2069 out.push_str(rest);
2070 Ok(out)
2071 }
2072
2073 async fn register_python_udf_directive(&self, body: &str, is_aggregate: bool) -> SqlResult<()> {
2076 use base64::Engine as _;
2077 let mut parts = body.splitn(4, ':');
2078 let (name, in_types, out_type, pickle_b64) =
2079 match (parts.next(), parts.next(), parts.next(), parts.next()) {
2080 (Some(n), Some(i), Some(o), Some(p)) => (n, i, o, p),
2081 _ => {
2082 return Err(SqlError::DataFusion {
2083 message: "malformed python-udf directive".into(),
2084 });
2085 }
2086 };
2087 let input_types: Vec<String> = if in_types.is_empty() {
2088 Vec::new()
2089 } else {
2090 in_types.split(',').map(str::to_string).collect()
2091 };
2092 let pickle = base64::engine::general_purpose::STANDARD
2093 .decode(pickle_b64)
2094 .map_err(|e| SqlError::DataFusion {
2095 message: format!("invalid python-udf pickle base64: {e}"),
2096 })?;
2097 if is_aggregate {
2098 self.register_python_udaf(name, &pickle, &input_types, out_type)
2099 .await
2100 } else {
2101 self.register_python_udf(name, &pickle, &input_types, out_type)
2102 .await
2103 }
2104 }
2105
2106 pub async fn register_python_udf(
2112 &self,
2113 name: &str,
2114 pickle: &[u8],
2115 input_types: &[String],
2116 output_type: &str,
2117 ) -> SqlResult<()> {
2118 use arrow::datatypes::{Field, Schema};
2119 let Some(registry) = &self.udf_registry else {
2120 return Err(SqlError::DataFusion {
2121 message: "cannot register a python UDF: engine has no UDF registry".into(),
2122 });
2123 };
2124 let input_fields: Vec<Field> = input_types
2125 .iter()
2126 .enumerate()
2127 .map(|(i, t)| Field::new(format!("a{i}"), python_udf_arrow_type(t), true))
2128 .collect();
2129 let input_schema = Schema::new(input_fields);
2130 let output_field = Field::new("out", python_udf_arrow_type(output_type), true);
2131 let pool = crate::python_udf::global_pool().map_err(|e| SqlError::DataFusion {
2132 message: format!("python UDF worker unavailable: {e:?}"),
2133 })?;
2134 let udf = std::sync::Arc::new(crate::python_udf::PythonWorkerUdf::new(
2135 name,
2136 pickle.to_vec(),
2137 input_schema,
2138 output_field,
2139 pool,
2140 ));
2141 registry
2142 .write()
2143 .map_err(|e| SqlError::DataFusion {
2144 message: e.to_string(),
2145 })?
2146 .register_scalar(udf);
2147 self.bump_udf_version();
2148 self.sync_scalar_udfs().await
2149 }
2150
2151 pub async fn register_python_udaf(
2158 &self,
2159 name: &str,
2160 pickle: &[u8],
2161 input_types: &[String],
2162 output_type: &str,
2163 ) -> SqlResult<()> {
2164 use arrow::datatypes::{Field, Schema};
2165 let Some(registry) = &self.udf_registry else {
2166 return Err(SqlError::DataFusion {
2167 message: "cannot register a python UDAF: engine has no UDF registry".into(),
2168 });
2169 };
2170 let input_fields: Vec<Field> = input_types
2171 .iter()
2172 .enumerate()
2173 .map(|(i, t)| Field::new(format!("a{i}"), python_udf_arrow_type(t), true))
2174 .collect();
2175 let input_schema = Schema::new(input_fields);
2176 let output_field = Field::new("out", python_udf_arrow_type(output_type), true);
2177 let pool = crate::python_udf::global_pool().map_err(|e| SqlError::DataFusion {
2178 message: format!("python UDF worker unavailable: {e:?}"),
2179 })?;
2180 let udf = std::sync::Arc::new(crate::python_udf::PythonWorkerAggregateUdf::new(
2181 name,
2182 pickle.to_vec(),
2183 input_schema,
2184 output_field,
2185 pool,
2186 ));
2187 registry
2188 .write()
2189 .map_err(|e| SqlError::DataFusion {
2190 message: e.to_string(),
2191 })?
2192 .register_aggregate(udf);
2193 self.bump_udf_version();
2194 self.sync_aggregate_udfs().await
2195 }
2196
2197 pub async fn sync_scalar_udfs(&self) -> SqlResult<()> {
2198 let Some(registry) = &self.udf_registry else {
2199 return Ok(());
2200 };
2201 let guard = registry.read().map_err(|e| SqlError::DataFusion {
2202 message: e.to_string(),
2203 })?;
2204 let limits = self.udf_limits.clone().unwrap_or_default();
2205 udf::sync_scalar_udfs_with_limits(&self.context, &guard, limits).map_err(|e| {
2206 SqlError::DataFusion {
2207 message: e.to_string(),
2208 }
2209 })
2210 }
2211
2212 pub async fn sync_scalar_udfs_with_limits(
2217 &self,
2218 limits: krishiv_plan::udf::ResourceLimits,
2219 ) -> SqlResult<()> {
2220 self.sync_scalar_udfs_with_limits_for_profile(
2221 limits,
2222 krishiv_common::resolve_durability_profile(),
2223 )
2224 .await
2225 }
2226
2227 pub async fn sync_scalar_udfs_with_limits_for_profile(
2229 &self,
2230 limits: krishiv_plan::udf::ResourceLimits,
2231 profile: krishiv_common::DurabilityProfile,
2232 ) -> SqlResult<()> {
2233 self.sync_scalar_udfs_with_limits_for_policy(
2234 limits,
2235 krishiv_common::NativeScalarUdfPolicy::resolve(profile),
2236 )
2237 .await
2238 }
2239
2240 pub async fn sync_scalar_udfs_with_limits_for_policy(
2242 &self,
2243 limits: krishiv_plan::udf::ResourceLimits,
2244 policy: krishiv_common::NativeScalarUdfPolicy,
2245 ) -> SqlResult<()> {
2246 let Some(registry) = &self.udf_registry else {
2247 return Ok(());
2248 };
2249 let guard = registry.read().map_err(|e| SqlError::DataFusion {
2250 message: e.to_string(),
2251 })?;
2252 udf::sync_scalar_udfs_with_limits_for_policy(&self.context, &guard, limits, policy).map_err(
2253 |e| SqlError::DataFusion {
2254 message: e.to_string(),
2255 },
2256 )
2257 }
2258
2259 pub async fn sync_aggregate_udfs(&self) -> SqlResult<()> {
2261 let Some(registry) = &self.udf_registry else {
2262 return Ok(());
2263 };
2264 let guard = registry.read().map_err(|e| SqlError::DataFusion {
2265 message: e.to_string(),
2266 })?;
2267 udf::sync_aggregate_udfs(&self.context, &guard).map_err(|e| SqlError::DataFusion {
2268 message: e.to_string(),
2269 })
2270 }
2271
2272 pub async fn sync_table_udfs(&self) -> SqlResult<()> {
2274 let Some(registry) = &self.udf_registry else {
2275 return Ok(());
2276 };
2277 let guard = registry.read().map_err(|e| SqlError::DataFusion {
2278 message: e.to_string(),
2279 })?;
2280 udf::sync_table_udfs(&self.context, &guard).map_err(|e| SqlError::DataFusion {
2281 message: e.to_string(),
2282 })
2283 }
2284
2285 pub async fn sync_all_udfs(&self) -> SqlResult<()> {
2287 self.sync_scalar_udfs().await?;
2288 self.sync_aggregate_udfs().await?;
2289 self.sync_table_udfs().await?;
2290 Ok(())
2291 }
2292
2293 pub(crate) fn register_s3_object_store_for_warehouse(&self, path: &str) -> Result<(), String> {
2300 if !(path.starts_with("s3://") || path.starts_with("s3a://")) {
2301 return Ok(());
2302 }
2303 let url = url::Url::parse(path).map_err(|e| format!("invalid s3 url {path}: {e}"))?;
2304 let bucket = url.host_str().unwrap_or_default();
2305 let store_url = url::Url::parse(&format!("s3://{bucket}"))
2307 .map_err(|e| format!("invalid s3 bucket url: {e}"))?;
2308 let store = build_s3_object_store(bucket).map_err(|e| format!("s3 store init: {e}"))?;
2309 self.context.register_object_store(&store_url, store);
2310 Ok(())
2311 }
2312
2313 pub async fn register_parquet(
2315 &self,
2316 table_name: impl AsRef<str>,
2317 path: impl AsRef<Path>,
2318 ) -> SqlResult<()> {
2319 self.register_parquet_with_primary_key(table_name, path, &[] as &[String])
2320 .await
2321 }
2322
2323 pub async fn register_parquet_with_primary_key<S: AsRef<str>>(
2341 &self,
2342 table_name: impl AsRef<str>,
2343 path: impl AsRef<Path>,
2344 primary_key: &[S],
2345 ) -> SqlResult<()> {
2346 let table_name = table_name.as_ref();
2347 if table_name.trim().is_empty() {
2348 return Err(SqlError::EmptyTableName);
2349 }
2350
2351 let path = path.as_ref().to_string_lossy().into_owned();
2352
2353 self.register_s3_object_store_for_warehouse(&path)
2356 .map_err(|message| SqlError::DataFusion { message })?;
2357
2358 if self
2359 .context
2360 .table_exist(table_name)
2361 .map_err(SqlError::from)?
2362 {
2363 let _ = self
2364 .context
2365 .deregister_table(table_name)
2366 .map_err(SqlError::from)?;
2367 }
2368 let spec = crate::distributed_plan::ParquetTableSpec::new(table_name, path)
2375 .with_primary_key(primary_key.iter().map(|c| c.as_ref().to_owned()));
2376 crate::distributed_plan::register_parquet_table(&self.context, &spec).await?;
2377 if let Ok(provider) = self.context.table_provider(table_name).await
2379 && let Some(stats) = provider.statistics()
2380 && let Some(n) = stats.num_rows.get_value()
2381 && let Ok(mut counts) = self.table_row_counts.write()
2382 {
2383 counts.insert(table_name.to_string(), *n as u64);
2384 }
2385 self.invalidate_plan_cache();
2386 Ok(())
2387 }
2388
2389 pub async fn read_parquet(&self, path: impl AsRef<Path>) -> SqlResult<SqlDataFrame> {
2391 let path = path.as_ref().to_string_lossy().into_owned();
2392 let dataframe = self
2393 .context
2394 .read_parquet(path, ParquetReadOptions::default())
2395 .await?;
2396 Ok(self.make_sql_df("parquet-read", dataframe))
2397 }
2398
2399 pub async fn register_record_batches(
2405 &self,
2406 table_name: impl AsRef<str>,
2407 batches: Vec<RecordBatch>,
2408 ) -> SqlResult<()> {
2409 use std::sync::Arc;
2410 let table_name = table_name.as_ref();
2411 if table_name.trim().is_empty() {
2412 return Err(SqlError::EmptyTableName);
2413 }
2414 if batches.is_empty() {
2415 return Ok(());
2416 }
2417 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
2418 let schema = batches
2419 .first()
2420 .ok_or_else(|| SqlError::DataFusion {
2421 message: "empty batch list".into(),
2422 })?
2423 .schema();
2424 let mem_table =
2425 datafusion::datasource::MemTable::try_new(schema, vec![batches]).map_err(|e| {
2426 SqlError::DataFusion {
2427 message: e.to_string(),
2428 }
2429 })?;
2430 if self
2431 .context
2432 .table_exist(table_name)
2433 .map_err(SqlError::from)?
2434 {
2435 let _ = self
2436 .context
2437 .deregister_table(table_name)
2438 .map_err(SqlError::from)?;
2439 }
2440 self.context
2441 .register_table(table_name, Arc::new(mem_table))
2442 .map_err(|e| SqlError::DataFusion {
2443 message: e.to_string(),
2444 })?;
2445 if total_rows > 0
2446 && let Ok(mut counts) = self.table_row_counts.write()
2447 {
2448 counts.insert(table_name.to_string(), total_rows as u64);
2449 }
2450 self.invalidate_plan_cache();
2451 Ok(())
2452 }
2453
2454 pub async fn read_parquet_with_options(
2456 &self,
2457 path: impl AsRef<Path>,
2458 opts: &ParquetReaderOptions,
2459 ) -> SqlResult<SqlDataFrame> {
2460 let path = path.as_ref().to_string_lossy().into_owned();
2461 let mut options = datafusion::prelude::ParquetReadOptions::default();
2462 if opts.batch_size.is_some() {
2463 options = options.parquet_pruning(true);
2464 }
2465 let dataframe = self.context.read_parquet(path, options).await?;
2471 Ok(self.make_sql_df("parquet-read", dataframe))
2472 }
2473
2474 pub async fn read_csv(&self, path: impl AsRef<Path>) -> SqlResult<SqlDataFrame> {
2476 self.read_csv_with_options(path, &CsvReaderOptions::default())
2477 .await
2478 }
2479
2480 pub async fn read_csv_with_options(
2482 &self,
2483 path: impl AsRef<Path>,
2484 opts: &CsvReaderOptions,
2485 ) -> SqlResult<SqlDataFrame> {
2486 let path = path.as_ref().to_string_lossy().into_owned();
2487 let mut options = datafusion::prelude::CsvReadOptions::new();
2488 if let Some(delim) = opts.delimiter {
2489 options = options.delimiter(delim as u8);
2490 }
2491 if let Some(has_header) = opts.has_header {
2492 options = options.has_header(has_header);
2493 }
2494 let dataframe = self.context.read_csv(path, options).await?;
2495 Ok(self.make_sql_df("csv-read", dataframe))
2496 }
2497
2498 pub async fn read_json(&self, path: impl AsRef<Path>) -> SqlResult<SqlDataFrame> {
2500 let path = path.as_ref().to_string_lossy().into_owned();
2501 let dataframe = self
2502 .context
2503 .read_json(path, datafusion::prelude::JsonReadOptions::default())
2504 .await?;
2505 Ok(self.make_sql_df("json-read", dataframe))
2506 }
2507
2508 pub async fn read_delta(
2510 &self,
2511 path: impl AsRef<str>,
2512 version: Option<i64>,
2513 ) -> SqlResult<SqlDataFrame> {
2514 let path = path.as_ref();
2515 let base = path.replace(['/', '.', '-'], "_");
2516 let table = match version {
2517 Some(v) => format!("delta_{base}_v{v}"),
2518 None => format!("delta_{base}"),
2519 };
2520 lakehouse::register_delta_uri(&self.context, &table, path, version).await?;
2521 self.sql(format!("SELECT * FROM {table}")).await
2522 }
2523
2524 pub async fn read_hudi(
2526 &self,
2527 path: impl AsRef<str>,
2528 query_type: krishiv_connectors::lakehouse::HudiQueryType,
2529 begin_instant: Option<&str>,
2530 ) -> SqlResult<SqlDataFrame> {
2531 let path = path.as_ref();
2532 let table = format!("hudi_{}", path.replace(['/', '.', '-'], "_"));
2533 lakehouse::register_hudi_uri(&self.context, &table, path, query_type, begin_instant)
2534 .await?;
2535 self.sql(format!("SELECT * FROM {table}")).await
2536 }
2537
2538 pub fn sql<'a>(
2548 &'a self,
2549 query: impl AsRef<str> + Send + 'a,
2550 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = SqlResult<SqlDataFrame>> + Send + 'a>>
2551 {
2552 let query: String = query.as_ref().to_owned();
2553 Box::pin(self.sql_boxed_body(query))
2554 }
2555
2556 async fn sql_boxed_body(&self, query: String) -> SqlResult<SqlDataFrame> {
2558 let query = query.as_str();
2559 if query.trim().is_empty() {
2560 return Err(SqlError::EmptyQuery);
2561 }
2562
2563 let script = split_sql_statements(query);
2572 if script.len() > 1
2573 && let [setup @ .., last_stmt] = script.as_slice()
2574 {
2575 for stmt in setup {
2576 Box::pin(self.sql(stmt.as_str())).await?.collect().await?;
2580 }
2581 return Box::pin(self.sql(last_stmt.as_str())).await;
2582 }
2583
2584 {
2588 let current = self.udf_registry_version.load(Ordering::Acquire);
2589 let last = self.udf_last_synced_version.load(Ordering::Relaxed);
2590 if current != last {
2591 self.sync_all_udfs().await?;
2592 self.udf_last_synced_version
2593 .store(current, Ordering::Release);
2594 }
2595 }
2596
2597 if let Some(stmt) = introspection_sql::parse_introspection_statement(query)? {
2599 return match stmt {
2600 introspection_sql::IntrospectionStatement::Describe { table } => {
2601 let batch = introspection_sql::describe_table(&self.context, &table).await?;
2602 let describe_table_name = next_ephemeral_name("describe_result");
2603 lakehouse::register_scan_batches(
2604 &self.context,
2605 &describe_table_name,
2606 vec![batch],
2607 )
2608 .await?;
2609 let dataframe = self
2610 .context
2611 .sql(&format!("SELECT * FROM {describe_table_name}"))
2612 .await?;
2613 Ok(self.attach_query_metadata(self.make_sql_df("describe", dataframe), query))
2614 }
2615 introspection_sql::IntrospectionStatement::Explain { mode, query: inner } => {
2616 let text = introspection_sql::explain_query(&inner, mode)?;
2617 let batch = introspection_sql::explain_result_batch(&text)?;
2618 let explain_table = next_ephemeral_name("explain_result");
2619 lakehouse::register_scan_batches(&self.context, &explain_table, vec![batch])
2620 .await?;
2621 let dataframe = self
2622 .context
2623 .sql(&format!("SELECT * FROM {explain_table}"))
2624 .await?;
2625 Ok(self.attach_query_metadata(self.make_sql_df("explain", dataframe), query))
2626 }
2627 };
2628 }
2629
2630 if live_table::execute_live_table_ddl(&self.live_table_registry, query)?.is_some() {
2632 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2633 return Ok(self.attach_query_metadata(self.make_sql_df("live-table-ddl", empty), query));
2634 }
2635
2636 match incremental_view::execute_incremental_view_ddl(
2638 &self.incremental_view_registry,
2639 query,
2640 )? {
2641 Some(incremental_view::IncrementalViewResult::Refresh(_name)) => {
2642 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2645 return Ok(self.attach_query_metadata(
2646 self.make_sql_df("incremental-view-refresh", empty),
2647 query,
2648 ));
2649 }
2650 Some(_) => {
2651 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2652 return Ok(self.attach_query_metadata(
2653 self.make_sql_df("incremental-view-ddl", empty),
2654 query,
2655 ));
2656 }
2657 None => {}
2658 }
2659
2660 if let Some(ddl) = streaming_table_ddl::parse_create_streaming_table(query) {
2668 let _plan = streaming_window_plan::compile_streaming_window_sql(&ddl.query)?;
2669 return Err(SqlError::Unsupported {
2670 feature: format!(
2671 "CREATE STREAMING TABLE '{}' compiled to a continuous plan, but this session \
2672 has no streaming coordinator to run it; submit it via the continuous-stream \
2673 registration API or a cluster-attached session",
2674 ddl.name
2675 ),
2676 });
2677 }
2678
2679 if pipeline_ddl::execute_pipeline_ddl(&self.pipeline_registry, query)?.is_some() {
2683 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2684 return Ok(self.attach_query_metadata(self.make_sql_df("pipeline-ddl", empty), query));
2685 }
2686
2687 let trimmed = query.trim();
2690 if trimmed
2691 .to_ascii_uppercase()
2692 .starts_with("SET SHUFFLE.PARTITIONS")
2693 {
2694 let value = trimmed.split('=').nth(1).map(|s| s.trim()).unwrap_or("");
2695 match value.parse::<u32>() {
2696 Ok(n) if n > 0 => {
2697 {
2698 let mut guard =
2699 self.shuffle_partitions
2700 .write()
2701 .map_err(|e| SqlError::DataFusion {
2702 message: e.to_string(),
2703 })?;
2704 *guard = Some(n);
2705 }
2706 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2707 return Ok(self.make_sql_df("set-shuffle-partitions", empty));
2708 }
2709 Ok(_) => {
2710 {
2711 let mut guard =
2712 self.shuffle_partitions
2713 .write()
2714 .map_err(|e| SqlError::DataFusion {
2715 message: e.to_string(),
2716 })?;
2717 *guard = None;
2718 }
2719 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2720 return Ok(self.make_sql_df("set-shuffle-partitions", empty));
2721 }
2722 Err(_) => {
2723 return Err(SqlError::DataFusion {
2724 message: format!(
2725 "invalid shuffle.partitions value '{value}'; expected a positive integer"
2726 ),
2727 });
2728 }
2729 }
2730 }
2731
2732 if let Some(result) = statement_completion::apply_use(&self.context, query) {
2736 result.map_err(|message| SqlError::DataFusion { message })?;
2737 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2738 return Ok(self.attach_query_metadata(self.make_sql_df("use", empty), query));
2739 }
2740 if let Some(rewrite) = statement_completion::rewrite_show_databases(query) {
2741 let dataframe = self.context.sql(&rewrite).await?;
2742 return Ok(
2743 self.attach_query_metadata(self.make_sql_df("show-databases", dataframe), query)
2744 );
2745 }
2746
2747 if create_function_ddl::is_create_function_returns_table(query) {
2752 let ddl = create_function_ddl::parse_create_function(query)
2753 .map_err(|message| SqlError::InvalidTableFunction { message })?;
2754 if ddl.language.as_deref() != Some("sql") {
2755 return Err(SqlError::Unsupported {
2756 feature: format!(
2757 "CREATE FUNCTION '{}' uses language {:?}; only LANGUAGE SQL AS '...' \
2758 table functions are executable",
2759 ddl.function_name, ddl.language
2760 ),
2761 });
2762 }
2763 let body = ddl
2764 .body
2765 .as_deref()
2766 .filter(|body| !body.trim().is_empty())
2767 .ok_or_else(|| SqlError::InvalidTableFunction {
2768 message: format!(
2769 "SQL table function '{}' requires a non-empty AS body",
2770 ddl.function_name
2771 ),
2772 })?;
2773 let fields: Vec<_> = ddl
2774 .return_columns
2775 .iter()
2776 .map(|column| {
2777 arrow::datatypes::Field::new(&column.name, column.data_type.clone(), true)
2778 })
2779 .collect();
2780 let schema = arrow::datatypes::Schema::new(fields);
2781 let udf: std::sync::Arc<dyn krishiv_plan::udf::TableUdf> = std::sync::Arc::new(
2782 create_function_ddl::SqlBodyTableUdf::try_new(
2783 &ddl.function_name,
2784 schema,
2785 body,
2786 ddl.arguments.len(),
2787 std::sync::Arc::new(self.context.clone()),
2788 )
2789 .map_err(|error| SqlError::InvalidTableFunction {
2790 message: error.to_string(),
2791 })?,
2792 );
2793 if let Some(registry) = &self.udf_registry {
2794 let mut guard = registry.write().map_err(|e| SqlError::DataFusion {
2795 message: e.to_string(),
2796 })?;
2797 guard.register_table(std::sync::Arc::clone(&udf));
2798 }
2799 udf::register_single_table_udf(&self.context, std::sync::Arc::clone(&udf))
2800 .map_err(SqlError::from)?;
2801 self.bump_udf_version();
2804 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2805 return Ok(
2806 self.attach_query_metadata(self.make_sql_df("create-function", empty), query)
2807 );
2808 }
2809
2810 if query
2811 .trim_start()
2812 .to_ascii_uppercase()
2813 .starts_with("MERGE INTO")
2814 {
2815 let batches = lakehouse::execute_merge_sql(&self.context, query).await?;
2816 let merge_table = next_ephemeral_name("merge_result");
2817 lakehouse::register_scan_batches(&self.context, &merge_table, batches).await?;
2818 let dataframe = self
2819 .context
2820 .sql(&format!("SELECT * FROM {merge_table}"))
2821 .await?;
2822 return Ok(self.attach_query_metadata(self.make_sql_df("merge", dataframe), query));
2823 }
2824
2825 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2834 if trimmed.to_ascii_uppercase().starts_with("CREATE ")
2835 && let Some(parsed_ctas) = parse_ctas(trimmed)
2836 {
2837 let resolved = self.resolve_iceberg_table(&parsed_ctas.table_ref);
2838 if resolved.is_none() && !parsed_ctas.partition_by.is_empty() {
2841 return Err(SqlError::DataFusion {
2842 message: format!(
2843 "PARTITIONED BY requires an Iceberg catalog table; `{}` does not \
2844 resolve to a registered Iceberg catalog",
2845 parsed_ctas.table_ref
2846 ),
2847 });
2848 }
2849 if let Some((iceberg_catalog, table_ident)) = resolved {
2850 return self
2851 .execute_iceberg_ctas(iceberg_catalog, table_ident, parsed_ctas, query)
2852 .await;
2853 }
2854 }
2855
2856 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2859 if trimmed.to_ascii_uppercase().starts_with("CALL SYSTEM.") {
2860 let result = self.dispatch_call_system(trimmed).await?;
2861 let call_table = next_ephemeral_name("call_result");
2862 lakehouse::register_scan_batches(&self.context, &call_table, vec![result]).await?;
2863 let dataframe = self
2864 .context
2865 .sql(&format!("SELECT * FROM {call_table}"))
2866 .await?;
2867 return Ok(self.attach_query_metadata(self.make_sql_df("call", dataframe), query));
2868 }
2869
2870 if trimmed
2876 .get(..14)
2877 .is_some_and(|p| p.eq_ignore_ascii_case("ANALYZE TABLE "))
2878 {
2879 let result = self.dispatch_analyze_table(trimmed).await?;
2880 let res_table = next_ephemeral_name("analyze_result");
2881 lakehouse::register_scan_batches(&self.context, &res_table, vec![result]).await?;
2882 let dataframe = self
2883 .context
2884 .sql(&format!("SELECT * FROM {res_table}"))
2885 .await?;
2886 return Ok(self.attach_query_metadata(self.make_sql_df("analyze", dataframe), query));
2887 }
2888
2889 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2893 if trimmed.to_ascii_uppercase().starts_with("DELETE FROM ")
2894 && let Some((table_ref, predicate)) = parse_dml_delete(trimmed)
2895 && let Some((iceberg_catalog, table_ident)) = self.resolve_iceberg_table(&table_ref)
2896 {
2897 use arrow::array::{ArrayRef, Int64Array};
2898 use arrow::datatypes::{DataType, Field, Schema};
2899 let (deleted, _) = krishiv_connectors::lakehouse::dml::iceberg_delete_where(
2900 iceberg_catalog,
2901 &table_ident,
2902 &predicate,
2903 &self.context,
2904 )
2905 .await
2906 .map_err(|e| SqlError::DataFusion {
2907 message: e.to_string(),
2908 })?;
2909 self.adjust_table_row_count_stat(&table_ref, -(deleted as i64));
2911 let schema = Arc::new(Schema::new(vec![Field::new(
2912 "deleted_rows",
2913 DataType::Int64,
2914 false,
2915 )]));
2916 let array: ArrayRef = Arc::new(Int64Array::from(vec![deleted as i64]));
2917 let batch =
2918 RecordBatch::try_new(schema, vec![array]).map_err(|e| SqlError::DataFusion {
2919 message: e.to_string(),
2920 })?;
2921 let res_table = next_ephemeral_name("delete_result");
2922 lakehouse::register_scan_batches(&self.context, &res_table, vec![batch]).await?;
2923 let dataframe = self
2924 .context
2925 .sql(&format!("SELECT * FROM {res_table}"))
2926 .await?;
2927 return Ok(self.attach_query_metadata(self.make_sql_df("delete", dataframe), query));
2928 }
2929
2930 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2932 if trimmed.to_ascii_uppercase().starts_with("UPDATE ")
2933 && let Some(parsed) = parse_dml_update(trimmed)
2934 && let Some((iceberg_catalog, table_ident)) =
2935 self.resolve_iceberg_table(&parsed.table_ref)
2936 {
2937 use arrow::array::{ArrayRef, Int64Array};
2938 use arrow::datatypes::{DataType, Field, Schema};
2939 let borrowed: Vec<(&str, &str)> = parsed
2940 .assignments
2941 .iter()
2942 .map(|(c, e)| (c.as_str(), e.as_str()))
2943 .collect();
2944 let pred = parsed.predicate.as_deref();
2945 let (updated, _) = krishiv_connectors::lakehouse::dml::iceberg_update_where(
2946 iceberg_catalog,
2947 &table_ident,
2948 &borrowed,
2949 pred,
2950 &self.context,
2951 )
2952 .await
2953 .map_err(|e| SqlError::DataFusion {
2954 message: e.to_string(),
2955 })?;
2956 let schema = Arc::new(Schema::new(vec![Field::new(
2957 "updated_rows",
2958 DataType::Int64,
2959 false,
2960 )]));
2961 let array: ArrayRef = Arc::new(Int64Array::from(vec![updated as i64]));
2962 let batch =
2963 RecordBatch::try_new(schema, vec![array]).map_err(|e| SqlError::DataFusion {
2964 message: e.to_string(),
2965 })?;
2966 let res_table = next_ephemeral_name("update_result");
2967 lakehouse::register_scan_batches(&self.context, &res_table, vec![batch]).await?;
2968 let dataframe = self
2969 .context
2970 .sql(&format!("SELECT * FROM {res_table}"))
2971 .await?;
2972 return Ok(self.attach_query_metadata(self.make_sql_df("update", dataframe), query));
2973 }
2974
2975 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2985 if trimmed.to_ascii_uppercase().starts_with("INSERT ")
2986 && let Some(parsed) = parse_dml_insert(trimmed)
2987 && parsed.columns.is_empty()
2988 && let Some((iceberg_catalog, table_ident)) =
2989 self.resolve_iceberg_table(&parsed.table_ref)
2990 {
2991 use arrow::array::{ArrayRef, Int64Array};
2992 use arrow::datatypes::{DataType, Field, Schema};
2993 let source_df = self.context.sql(&parsed.inner_query).await?;
2994 let stream = source_df
2995 .execute_stream()
2996 .await
2997 .map_err(|e| SqlError::DataFusion {
2998 message: e.to_string(),
2999 })?;
3000 let report = krishiv_connectors::lakehouse::dml::iceberg_append_into(
3001 iceberg_catalog,
3002 &table_ident,
3003 stream,
3004 )
3005 .await
3006 .map_err(|e| SqlError::DataFusion {
3007 message: e.to_string(),
3008 })?;
3009 self.adjust_table_row_count_stat(&parsed.table_ref, report.rows as i64);
3011 let schema = Arc::new(Schema::new(vec![Field::new(
3012 "inserted_rows",
3013 DataType::Int64,
3014 false,
3015 )]));
3016 let array: ArrayRef = Arc::new(Int64Array::from(vec![report.rows as i64]));
3017 let batch =
3018 RecordBatch::try_new(schema, vec![array]).map_err(|e| SqlError::DataFusion {
3019 message: e.to_string(),
3020 })?;
3021 let res_table = next_ephemeral_name("insert_result");
3022 lakehouse::register_scan_batches(&self.context, &res_table, vec![batch]).await?;
3023 let dataframe = self
3024 .context
3025 .sql(&format!("SELECT * FROM {res_table}"))
3026 .await?;
3027 return Ok(self.attach_query_metadata(self.make_sql_df("insert", dataframe), query));
3028 }
3029
3030 if query.to_ascii_uppercase().contains(" MATCH_RECOGNIZE ")
3034 && let Some(stmt) = cep_sql::parse_match_recognize(query)?
3035 {
3036 let is_streaming = self.is_streaming_source(&stmt.source_table);
3037 let streaming_limit = streaming_match_recognize_limit_from_env();
3045 let source_sql = if is_streaming {
3046 format!(
3047 "SELECT * FROM {} LIMIT {}",
3048 stmt.source_table, streaming_limit
3049 )
3050 } else {
3051 format!("SELECT * FROM {}", stmt.source_table)
3052 };
3053 let source_df = self.context.sql(&source_sql).await?;
3054 let source_batches = source_df.collect().await?;
3055 if is_streaming {
3056 tracing::warn!(
3057 source = %stmt.source_table,
3058 limit = streaming_limit,
3059 collected_rows = source_batches.iter().map(|b| b.num_rows()).sum::<usize>(),
3060 "MATCH_RECOGNIZE executed against a streaming source under \
3061 bounded materialisation; results only cover the first {0} rows \
3062 of the source. Set KRISHIV_MATCH_RECOGNIZE_STREAMING_LIMIT to a \
3063 larger value if your executor has the memory budget.",
3064 streaming_limit
3065 );
3066 }
3067 let results = cep_sql::execute_match_recognize(stmt, &source_batches)?;
3068 let cep_table = next_ephemeral_name("cep_result");
3069 lakehouse::register_scan_batches(&self.context, &cep_table, results).await?;
3070 let dataframe = self
3071 .context
3072 .sql(&format!("SELECT * FROM {cep_table}"))
3073 .await?;
3074 return Ok(self.attach_query_metadata(self.make_sql_df("cep", dataframe), query));
3075 }
3076
3077 let piped = pipe_syntax::process_pipe_syntax(query).map_err(|error| {
3086 SqlError::Unsupported {
3087 feature: error.to_string(),
3088 }
3089 })?;
3090 let query: &str = &piped;
3091
3092 let query = &pivot_sql::rewrite_pivot_unpivot(query)?;
3093
3094 let query = &streaming_tvf::rewrite_window_tvfs(query);
3096
3097 let (rewritten, as_ofs) =
3098 lakehouse::preprocess_as_of_sql(query).unwrap_or_else(|_| (query.to_string(), vec![]));
3099 lakehouse::apply_as_of_refs(&self.context, &as_ofs).await?;
3100
3101 let can_cache = as_ofs.is_empty();
3108 let shuffle_override = self
3109 .shuffle_partitions
3110 .read()
3111 .map(|g| *g)
3112 .unwrap_or_else(|e| *e.into_inner());
3113 if can_cache {
3114 let cached_plan: Option<datafusion::logical_expr::LogicalPlan> = self
3116 .plan_cache
3117 .lock()
3118 .unwrap_or_else(|e| e.into_inner())
3119 .get(&rewritten)
3120 .cloned();
3121 if let Some(plan) = cached_plan {
3122 let dataframe = self.context.execute_logical_plan(plan).await?;
3123 return Ok(self.attach_query_metadata(
3124 self.make_sql_df("sql-query", dataframe)
3125 .with_shuffle_partitions(shuffle_override),
3126 &rewritten,
3127 ));
3128 }
3129 }
3130
3131 if let Some(location) = extract_create_external_table_location(&rewritten) {
3140 self.register_s3_object_store_for_warehouse(&location)
3141 .map_err(|message| SqlError::DataFusion { message })?;
3142 }
3143
3144 let dataframe = self.context.sql(&rewritten).await?;
3145
3146 if let Some(table_name) = extract_create_external_table_name(&rewritten)
3150 && !table_name.is_empty()
3151 && let Ok(provider) = self.context.table_provider(&table_name).await
3152 {
3153 let maybe_rows = provider
3154 .statistics()
3155 .and_then(|s| s.num_rows.get_value().copied());
3156 if let Some(n) = maybe_rows
3157 && let Ok(mut counts) = self.table_row_counts.write()
3158 {
3159 counts.entry(table_name).or_insert(n as u64);
3160 }
3161 }
3162
3163 if can_cache {
3165 let plan = dataframe.logical_plan().clone();
3166 match self.plan_cache.lock() {
3167 Ok(mut cache) => cache.insert(rewritten.clone(), plan),
3168 Err(poisoned) => poisoned.into_inner().insert(rewritten.clone(), plan),
3169 }
3170 }
3171
3172 Ok(self.attach_query_metadata(
3173 self.make_sql_df("sql-query", dataframe)
3174 .with_shuffle_partitions(shuffle_override),
3175 &rewritten,
3176 ))
3177 }
3178
3179 pub async fn execute_with_timeout(
3186 &self,
3187 query: impl AsRef<str> + Send,
3188 timeout_ms: u64,
3189 ) -> SqlResult<SqlDataFrame> {
3190 let timeout = std::time::Duration::from_millis(timeout_ms);
3191 tokio::time::timeout(timeout, self.sql(query))
3192 .await
3193 .map_err(|_| SqlError::Timeout { timeout_ms })?
3194 }
3195
3196 pub async fn execute_with_operation_id(
3203 &self,
3204 operation_id: u64,
3205 query: impl AsRef<str> + Send,
3206 cancelled_ids: &OperationRegistry,
3207 ) -> SqlResult<TaggedQueryResult> {
3208 if cancelled_ids.is_cancelled(operation_id) {
3209 return Err(SqlError::OperationCancelled { operation_id });
3210 }
3211 let df = self.sql(query).await?;
3212 Ok(TaggedQueryResult {
3213 operation_id,
3214 inner: df,
3215 })
3216 }
3217
3218 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
3224 fn resolve_iceberg_table(
3225 &self,
3226 table_ref: &str,
3227 ) -> Option<(Arc<dyn iceberg::Catalog + Send + Sync>, iceberg::TableIdent)> {
3228 let parts: Vec<&str> = table_ref.splitn(3, '.').collect();
3229 let (catalog_arc, ns_str, table_str) = {
3230 let guard = self
3231 .iceberg_catalogs
3232 .read()
3233 .unwrap_or_else(|e| e.into_inner());
3234 if guard.is_empty() {
3235 return None;
3236 }
3237 match parts.len() {
3238 2 => {
3239 let (cat, _) = guard.first()?;
3240 (Arc::clone(cat), *parts.first()?, *parts.get(1)?)
3241 }
3242 3 => {
3243 let cat_name = parts.first().copied()?;
3244 let (cat, _) = guard.iter().find(|(_, n)| n == cat_name)?;
3245 (Arc::clone(cat), *parts.get(1)?, *parts.get(2)?)
3246 }
3247 _ => return None,
3248 }
3249 };
3250 let ns = iceberg::NamespaceIdent::from_vec(vec![ns_str.to_string()]).ok()?;
3251 let ident = iceberg::TableIdent::new(ns, table_str.to_string());
3252 Some((catalog_arc.as_iceberg(), ident))
3253 }
3254
3255 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
3261 async fn execute_iceberg_ctas(
3262 &self,
3263 iceberg_catalog: Arc<dyn iceberg::Catalog + Send + Sync>,
3264 table_ident: iceberg::TableIdent,
3265 parsed_ctas: ParsedCtas,
3266 query: &str,
3267 ) -> SqlResult<SqlDataFrame> {
3268 use arrow::array::{ArrayRef, Int64Array};
3269 use arrow::datatypes::{DataType, Field, Schema};
3270 use krishiv_connectors::lakehouse::partitioned_write::parse_partition_transform;
3271
3272 let partition_by = parsed_ctas
3273 .partition_by
3274 .iter()
3275 .map(|item| parse_partition_transform(item))
3276 .collect::<Result<Vec<_>, _>>()
3277 .map_err(|e| SqlError::DataFusion {
3278 message: e.to_string(),
3279 })?;
3280
3281 let dataframe = self.context.sql(&parsed_ctas.inner_query).await?;
3282 let stream = dataframe
3283 .execute_stream()
3284 .await
3285 .map_err(|e| SqlError::DataFusion {
3286 message: e.to_string(),
3287 })?;
3288 let report = krishiv_connectors::lakehouse::dml::land_ctas(
3289 iceberg_catalog,
3290 &table_ident,
3291 parsed_ctas.or_replace,
3292 &partition_by,
3293 stream,
3294 )
3295 .await
3296 .map_err(|e| SqlError::DataFusion {
3297 message: e.to_string(),
3298 })?;
3299 self.invalidate_plan_cache();
3301 self.record_table_row_count_stat(&parsed_ctas.table_ref, report.rows as u64);
3303
3304 let schema = Arc::new(Schema::new(vec![
3305 Field::new("rows_written", DataType::Int64, false),
3306 Field::new("bytes_written", DataType::Int64, false),
3307 Field::new("data_files", DataType::Int64, false),
3308 Field::new("snapshot_id", DataType::Int64, false),
3309 ]));
3310 let columns: Vec<ArrayRef> = vec![
3311 Arc::new(Int64Array::from(vec![report.rows as i64])),
3312 Arc::new(Int64Array::from(vec![report.bytes as i64])),
3313 Arc::new(Int64Array::from(vec![report.data_files as i64])),
3314 Arc::new(Int64Array::from(vec![report.snapshot_id])),
3315 ];
3316 let batch = RecordBatch::try_new(schema, columns).map_err(|e| SqlError::DataFusion {
3317 message: e.to_string(),
3318 })?;
3319 let res_table = next_ephemeral_name("ctas_result");
3320 lakehouse::register_scan_batches(&self.context, &res_table, vec![batch]).await?;
3321 let dataframe = self
3322 .context
3323 .sql(&format!("SELECT * FROM {res_table}"))
3324 .await?;
3325 Ok(self.attach_query_metadata(self.make_sql_df("ctas", dataframe), query))
3326 }
3327
3328 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
3333 fn record_table_row_count_stat(&self, table_ref: &str, row_count: u64) {
3334 let registry = krishiv_plan::optimizer::global_table_stats();
3335 let mut names = vec![table_ref];
3336 let bare = table_ref.rsplit('.').next().unwrap_or(table_ref);
3337 if bare != table_ref {
3338 names.push(bare);
3339 }
3340 for name in &names {
3341 let mut stats = registry
3342 .get(name)
3343 .unwrap_or_else(|| krishiv_plan::optimizer::TableCboStats::new(*name));
3344 stats.row_count = Some(row_count);
3345 registry.put(stats);
3346 }
3347 if let Ok(mut counts) = self.table_row_counts.write() {
3348 for name in &names {
3349 counts.insert((*name).to_owned(), row_count);
3350 }
3351 }
3352 }
3353
3354 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
3359 fn adjust_table_row_count_stat(&self, table_ref: &str, delta: i64) {
3360 let registry = krishiv_plan::optimizer::global_table_stats();
3361 let mut names = vec![table_ref];
3362 let bare = table_ref.rsplit('.').next().unwrap_or(table_ref);
3363 if bare != table_ref {
3364 names.push(bare);
3365 }
3366 for name in &names {
3367 if let Some(mut stats) = registry.get(name)
3368 && let Some(current) = stats.row_count
3369 {
3370 stats.row_count = Some(current.saturating_add_signed(delta));
3371 registry.put(stats);
3372 }
3373 }
3374 if let Ok(mut counts) = self.table_row_counts.write() {
3375 for name in &names {
3376 if let Some(current) = counts.get(*name).copied() {
3377 counts.insert((*name).to_owned(), current.saturating_add_signed(delta));
3378 }
3379 }
3380 }
3381 }
3382
3383 async fn dispatch_analyze_table(&self, stmt: &str) -> SqlResult<RecordBatch> {
3394 use arrow::array::{ArrayRef, Int64Array, StringArray};
3395 use arrow::datatypes::{DataType, Field, Schema};
3396
3397 let rest = stmt
3398 .get(14..)
3399 .unwrap_or("")
3400 .trim()
3401 .trim_end_matches(';')
3402 .trim();
3403 let (table_ref, tail) = match rest.split_once(char::is_whitespace) {
3404 Some((t, tail)) => (t.trim(), tail.trim()),
3405 None => (rest, ""),
3406 };
3407 if table_ref.is_empty() {
3408 return Err(SqlError::DataFusion {
3409 message: String::from("ANALYZE TABLE: table reference is required"),
3410 });
3411 }
3412 let mut tail = tail;
3414 if tail
3415 .get(..18)
3416 .is_some_and(|p| p.eq_ignore_ascii_case("COMPUTE STATISTICS"))
3417 {
3418 tail = tail.get(18..).unwrap_or("").trim();
3419 }
3420 let columns: Vec<String> = if tail
3421 .get(..11)
3422 .is_some_and(|p| p.eq_ignore_ascii_case("FOR COLUMNS"))
3423 {
3424 tail.get(11..)
3425 .unwrap_or("")
3426 .trim()
3427 .trim_start_matches('(')
3428 .trim_end_matches(')')
3429 .split(',')
3430 .map(|c| c.trim().trim_matches('"').to_owned())
3431 .filter(|c| !c.is_empty())
3432 .collect()
3433 } else if tail.is_empty() {
3434 Vec::new()
3435 } else {
3436 return Err(SqlError::DataFusion {
3437 message: format!("ANALYZE TABLE: unexpected trailing clause: {tail}"),
3438 });
3439 };
3440
3441 let mut projections = vec![String::from("count(*)")];
3443 for c in &columns {
3444 projections.push(format!("approx_distinct(\"{c}\")"));
3445 projections.push(format!("min(\"{c}\")"));
3446 projections.push(format!("max(\"{c}\")"));
3447 projections.push(format!("count(\"{c}\")"));
3448 }
3449 let scan_sql = format!("SELECT {} FROM {table_ref}", projections.join(", "));
3450 let batches = self.context.sql(&scan_sql).await?.collect().await?;
3451 let row =
3452 batches
3453 .iter()
3454 .find(|b| b.num_rows() > 0)
3455 .ok_or_else(|| SqlError::DataFusion {
3456 message: format!("ANALYZE TABLE {table_ref}: aggregation returned no rows"),
3457 })?;
3458 let cell_string = |col: usize| -> Option<String> {
3459 let column = row.columns().get(col)?;
3460 if column.is_null(0) {
3461 return None;
3462 }
3463 arrow::util::display::array_value_to_string(column, 0).ok()
3464 };
3465 let cell_u64 = |col: usize| -> Option<u64> { cell_string(col)?.parse().ok() };
3466 let row_count = cell_u64(0).ok_or_else(|| SqlError::DataFusion {
3467 message: format!("ANALYZE TABLE {table_ref}: COUNT(*) unreadable"),
3468 })?;
3469
3470 let mut column_stats = Vec::with_capacity(columns.len());
3471 for (i, name) in columns.iter().enumerate() {
3472 let base = 1 + i * 4;
3473 let non_null = cell_u64(base + 3);
3474 column_stats.push(krishiv_plan::optimizer::ColumnCboStats {
3475 name: name.clone(),
3476 ndv: cell_u64(base),
3477 min: cell_string(base + 1),
3478 max: cell_string(base + 2),
3479 null_count: non_null.map(|n| row_count.saturating_sub(n)),
3480 });
3481 }
3482
3483 let avg_row_bytes = match self.context.table_provider(table_ref).await {
3485 Ok(provider) => provider.statistics().and_then(|s| {
3486 let rows = s.num_rows.get_value().copied()?;
3487 let bytes = s.total_byte_size.get_value().copied()?;
3488 (rows > 0).then(|| (bytes / rows) as u64)
3489 }),
3490 Err(_) => None,
3491 };
3492
3493 let mut stats =
3494 krishiv_plan::optimizer::TableCboStats::new(table_ref).with_row_count(row_count);
3495 if let Some(bytes) = avg_row_bytes {
3496 stats = stats.with_avg_row_bytes(bytes);
3497 }
3498 if let Some(max_ndv) = column_stats.iter().filter_map(|c| c.ndv).max() {
3499 stats = stats.with_ndv(max_ndv);
3501 }
3502 stats.columns = column_stats;
3503 let registry = krishiv_plan::optimizer::global_table_stats();
3504 let bare = table_ref.rsplit('.').next().unwrap_or(table_ref);
3507 if bare != table_ref {
3508 let mut bare_stats = stats.clone();
3509 bare_stats.table = bare.to_owned();
3510 registry.put(bare_stats);
3511 }
3512 let analyzed_columns = stats.columns.len();
3513 registry.put(stats);
3514 if let Ok(mut counts) = self.table_row_counts.write() {
3515 counts.insert(table_ref.to_owned(), row_count);
3516 if bare != table_ref {
3517 counts.insert(bare.to_owned(), row_count);
3518 }
3519 }
3520 self.invalidate_plan_cache();
3521
3522 let schema = Arc::new(Schema::new(vec![
3523 Field::new("table_name", DataType::Utf8, false),
3524 Field::new("row_count", DataType::Int64, false),
3525 Field::new("avg_row_bytes", DataType::Int64, true),
3526 Field::new("columns_analyzed", DataType::Int64, false),
3527 ]));
3528 let columns_out: Vec<ArrayRef> = vec![
3529 Arc::new(StringArray::from(vec![table_ref.to_owned()])),
3530 Arc::new(Int64Array::from(vec![row_count as i64])),
3531 Arc::new(Int64Array::from(vec![avg_row_bytes.map(|b| b as i64)])),
3532 Arc::new(Int64Array::from(vec![analyzed_columns as i64])),
3533 ];
3534 RecordBatch::try_new(schema, columns_out).map_err(|e| SqlError::DataFusion {
3535 message: e.to_string(),
3536 })
3537 }
3538
3539 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
3542 async fn dispatch_call_system(&self, stmt: &str) -> SqlResult<RecordBatch> {
3543 use arrow::array::{ArrayRef, Int64Array};
3544 use arrow::datatypes::{DataType, Field, Schema};
3545
3546 let upper = stmt.to_ascii_uppercase();
3547 const PREFIX: &str = "CALL SYSTEM.";
3548 let upper_after = &upper[PREFIX.len()..];
3549 let orig_after = &stmt[PREFIX.len()..];
3550
3551 let paren = upper_after.find('(').ok_or_else(|| SqlError::DataFusion {
3552 message: format!("CALL: missing '(' in: {stmt}"),
3553 })?;
3554 let proc_name = upper_after[..paren].trim();
3555
3556 let args_raw = orig_after[paren + 1..]
3557 .trim_end_matches(';')
3558 .trim()
3559 .trim_end_matches(')')
3560 .trim();
3561 let args = call_args_from_str(args_raw);
3562
3563 let iceberg_catalog = {
3564 let guard = self
3565 .iceberg_catalogs
3566 .read()
3567 .unwrap_or_else(|e| e.into_inner());
3568 guard
3569 .first()
3570 .ok_or_else(|| SqlError::DataFusion {
3571 message: "CALL system: no Iceberg catalog registered".to_string(),
3572 })?
3573 .0
3574 .as_iceberg()
3575 };
3576
3577 let table_ref = args.first().ok_or_else(|| SqlError::DataFusion {
3578 message: format!("CALL {proc_name}: table reference argument is required"),
3579 })?;
3580 let table_ident = iceberg_table_ident(table_ref)?;
3581
3582 if proc_name == "MAINTAIN_TABLE" {
3586 let older_than = parse_call_duration(args.get(1).map_or("7 days", |s| s.as_str()))?;
3587 let target_bytes = args
3588 .get(2)
3589 .and_then(|s| s.parse::<u64>().ok())
3590 .unwrap_or(128 * 1024 * 1024);
3591 let retain_last = args
3592 .get(3)
3593 .and_then(|s| s.parse::<usize>().ok())
3594 .unwrap_or(1);
3595 let report = krishiv_connectors::lakehouse::maintenance::maintain_table(
3596 iceberg_catalog,
3597 &table_ident,
3598 target_bytes,
3599 older_than,
3600 retain_last,
3601 )
3602 .await
3603 .map_err(|e| SqlError::DataFusion {
3604 message: e.to_string(),
3605 })?;
3606 let schema = Arc::new(Schema::new(vec![
3607 Field::new("compacted_files", DataType::Int64, false),
3608 Field::new("expired_snapshots", DataType::Int64, false),
3609 Field::new("removed_orphans", DataType::Int64, false),
3610 ]));
3611 let columns: Vec<ArrayRef> = vec![
3612 Arc::new(Int64Array::from(vec![report.compacted_files as i64])),
3613 Arc::new(Int64Array::from(vec![report.expired_snapshots as i64])),
3614 Arc::new(Int64Array::from(vec![report.removed_orphans as i64])),
3615 ];
3616 return RecordBatch::try_new(schema, columns).map_err(|e| SqlError::DataFusion {
3617 message: e.to_string(),
3618 });
3619 }
3620
3621 let count: i64 = match proc_name {
3622 "EXPIRE_SNAPSHOTS" => {
3623 let dur_s = args.get(1).ok_or_else(|| SqlError::DataFusion {
3624 message: "CALL expire_snapshots: duration argument is required".to_string(),
3625 })?;
3626 let older_than = parse_call_duration(dur_s)?;
3627 let retain_last = args
3628 .get(2)
3629 .and_then(|s| s.parse::<usize>().ok())
3630 .unwrap_or(1);
3631 krishiv_connectors::lakehouse::maintenance::expire_snapshots(
3632 iceberg_catalog,
3633 &table_ident,
3634 older_than,
3635 retain_last,
3636 )
3637 .await
3638 .map_err(|e| SqlError::DataFusion {
3639 message: e.to_string(),
3640 })? as i64
3641 }
3642 "REMOVE_ORPHAN_FILES" => {
3643 let dur_s = args.get(1).ok_or_else(|| SqlError::DataFusion {
3644 message: "CALL remove_orphan_files: duration argument is required".to_string(),
3645 })?;
3646 let older_than = parse_call_duration(dur_s)?;
3647 krishiv_connectors::lakehouse::maintenance::remove_orphan_files(
3648 iceberg_catalog,
3649 &table_ident,
3650 older_than,
3651 )
3652 .await
3653 .map_err(|e| SqlError::DataFusion {
3654 message: e.to_string(),
3655 })? as i64
3656 }
3657 "COMPACT_DATA_FILES" => {
3658 let target_bytes = args
3659 .get(1)
3660 .and_then(|s| s.parse::<u64>().ok())
3661 .unwrap_or(128 * 1024 * 1024);
3662 krishiv_connectors::lakehouse::maintenance::compact_data_files(
3663 iceberg_catalog,
3664 &table_ident,
3665 target_bytes,
3666 )
3667 .await
3668 .map_err(|e| SqlError::DataFusion {
3669 message: e.to_string(),
3670 })? as i64
3671 }
3672 other => {
3673 return Err(SqlError::Unsupported {
3674 feature: format!("CALL system.{other}: unknown procedure"),
3675 });
3676 }
3677 };
3678
3679 let col = match proc_name {
3680 "EXPIRE_SNAPSHOTS" => "expired_snapshots",
3681 "REMOVE_ORPHAN_FILES" => "removed_files",
3682 "COMPACT_DATA_FILES" => "rewritten_files",
3683 _ => "result",
3684 };
3685 let schema = Arc::new(Schema::new(vec![Field::new(col, DataType::Int64, false)]));
3686 let array: ArrayRef = Arc::new(Int64Array::from(vec![count]));
3687 RecordBatch::try_new(schema, vec![array]).map_err(|e| SqlError::DataFusion {
3688 message: e.to_string(),
3689 })
3690 }
3691}
3692
3693pub struct TaggedQueryResult {
3695 pub operation_id: u64,
3697 pub inner: SqlDataFrame,
3699}
3700
3701#[derive(Clone, Default)]
3707pub struct OperationRegistry {
3708 cancelled: Arc<std::sync::RwLock<std::collections::HashSet<u64>>>,
3709 progress: Arc<std::sync::RwLock<std::collections::HashMap<u64, (u64, u64)>>>,
3710}
3711
3712impl OperationRegistry {
3713 pub fn new() -> Self {
3715 Self::default()
3716 }
3717
3718 pub fn cancel(&self, operation_id: u64) {
3722 if let Ok(mut ids) = self.cancelled.write() {
3723 ids.insert(operation_id);
3724 }
3725 }
3726
3727 pub fn is_cancelled(&self, operation_id: u64) -> bool {
3729 self.cancelled
3730 .read()
3731 .map(|ids| ids.contains(&operation_id))
3732 .unwrap_or(false)
3733 }
3734
3735 pub fn remove(&self, operation_id: u64) {
3737 if let Ok(mut ids) = self.cancelled.write() {
3738 ids.remove(&operation_id);
3739 }
3740 if let Ok(mut progress) = self.progress.write() {
3741 progress.remove(&operation_id);
3742 }
3743 }
3744
3745 pub fn update_progress(&self, operation_id: u64, rows_scanned: u64, rows_emitted: u64) {
3747 if let Ok(mut progress) = self.progress.write() {
3748 progress.insert(operation_id, (rows_scanned, rows_emitted));
3749 }
3750 }
3751
3752 pub fn progress(&self, operation_id: u64) -> Option<(u64, u64)> {
3754 self.progress
3755 .read()
3756 .ok()
3757 .and_then(|progress| progress.get(&operation_id).copied())
3758 }
3759
3760 pub fn cancelled_ids(&self) -> Vec<u64> {
3762 self.cancelled
3763 .read()
3764 .map(|ids| ids.iter().copied().collect())
3765 .unwrap_or_default()
3766 }
3767}
3768
3769pub(crate) fn extract_create_external_table_name(query: &str) -> Option<String> {
3774 use datafusion::sql::parser::{DFParser, Statement as DFStatement};
3775 let mut stmts = DFParser::parse_sql(query).ok()?;
3776 match stmts.pop_front()? {
3777 DFStatement::CreateExternalTable(create) => Some(create.name.to_string()),
3778 _ => None,
3779 }
3780}
3781
3782pub(crate) fn extract_create_external_table_location(query: &str) -> Option<String> {
3790 use datafusion::sql::parser::{DFParser, Statement as DFStatement};
3791 let mut stmts = DFParser::parse_sql(query).ok()?;
3792 match stmts.pop_front()? {
3793 DFStatement::CreateExternalTable(create) => Some(create.location),
3794 _ => None,
3795 }
3796}
3797
3798pub enum GroupingMode<'a> {
3806 Sets(Vec<Vec<&'a krishiv_plan::expression::Expr>>),
3807 Cube(Vec<&'a krishiv_plan::expression::Expr>),
3808 Rollup(Vec<&'a krishiv_plan::expression::Expr>),
3809}
3810
3811#[async_trait::async_trait]
3812pub trait KrishivDataFrameOps: Send + Sync {
3813 async fn collect(&self) -> SqlResult<Vec<RecordBatch>>;
3815 async fn collect_with_stats(&self) -> SqlResult<(Vec<RecordBatch>, SqlExecutionStats)>;
3817 async fn explain(&self) -> SqlResult<String>;
3819
3820 async fn explain_analyze(&self) -> SqlResult<String> {
3826 Err(SqlError::DataFusion {
3827 message: String::from("EXPLAIN ANALYZE is not supported for this dataframe backend"),
3828 })
3829 }
3830 fn explain_logical(&self) -> String;
3832 fn krishiv_logical_plan(&self) -> LogicalPlan;
3834 fn query(&self) -> Option<&str>;
3836 fn to_sql(&self) -> SqlResult<String> {
3840 Err(SqlError::Unsupported {
3841 feature: "to_sql (plan unparsing) is not supported for this DataFrame".into(),
3842 })
3843 }
3844 async fn execute_stream(&self) -> SqlResult<SqlStream>;
3846
3847 fn schema(&self) -> SchemaRef;
3851
3852 async fn select(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3854
3855 async fn select_exprs(
3857 &self,
3858 expressions: &[&krishiv_plan::expression::Expr],
3859 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3860
3861 async fn unnest_columns(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3865
3866 async fn aggregate(
3868 &self,
3869 group_exprs: &[&krishiv_plan::expression::Expr],
3870 aggregate_exprs: &[&krishiv_plan::expression::Expr],
3871 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3872
3873 async fn aggregate_grouping(
3875 &self,
3876 grouping: GroupingMode<'_>,
3877 aggregate_exprs: &[&krishiv_plan::expression::Expr],
3878 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3879
3880 async fn pivot(
3882 &self,
3883 group_exprs: &[&krishiv_plan::expression::Expr],
3884 pivot_column: &krishiv_plan::expression::Expr,
3885 aggregate_expr: &krishiv_plan::expression::Expr,
3886 values: &[(krishiv_plan::expression::ScalarValue, String)],
3887 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3888
3889 async fn unpivot(
3891 &self,
3892 columns: &[&str],
3893 name_column: &str,
3894 value_column: &str,
3895 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3896
3897 async fn filter(&self, predicate: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3899
3900 async fn filter_expr(
3902 &self,
3903 predicate: &krishiv_plan::expression::Expr,
3904 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3905
3906 async fn limit(&self, n: usize) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3908
3909 async fn distinct(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3911
3912 async fn drop_nulls(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3914
3915 async fn sample(&self, fraction: f64) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3917
3918 async fn sort(
3920 &self,
3921 columns: &[&str],
3922 descending: &[bool],
3923 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3924
3925 async fn alias(&self, alias: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3927
3928 async fn drop_columns(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3930
3931 async fn rename_column(&self, old: &str, new: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3933
3934 async fn with_column(&self, name: &str, expr: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3936
3937 fn as_any(&self) -> &dyn std::any::Any;
3939
3940 async fn describe(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3942
3943 async fn fill_null(&self, column: &str, value: &str)
3945 -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3946
3947 async fn join(
3949 &self,
3950 right: &dyn KrishivDataFrameOps,
3951 how: &str,
3952 left_on: &[&str],
3953 right_on: &[&str],
3954 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3955
3956 async fn union(
3958 &self,
3959 right: &dyn KrishivDataFrameOps,
3960 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3961
3962 async fn union_distinct(
3963 &self,
3964 right: &dyn KrishivDataFrameOps,
3965 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3966
3967 async fn intersect(
3968 &self,
3969 right: &dyn KrishivDataFrameOps,
3970 distinct: bool,
3971 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3972
3973 async fn except(
3974 &self,
3975 right: &dyn KrishivDataFrameOps,
3976 distinct: bool,
3977 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3978
3979 async fn register_batches(&self, name: &str, batches: Vec<RecordBatch>) -> SqlResult<()>;
3982
3983 async fn deregister_table(&self, name: &str) -> SqlResult<()>;
3985
3986 async fn create_view(&self, name: &str, replace: bool) -> SqlResult<()>;
3989}
3990
3991fn df_plan_to_krishiv_nodes(
3999 plan: &datafusion::logical_expr::LogicalPlan,
4000 table_row_counts: &std::collections::HashMap<String, u64>,
4001 counter: &mut usize,
4002) -> (Vec<krishiv_plan::PlanNode>, String) {
4003 use datafusion::logical_expr::LogicalPlan as DfPlan;
4004 use krishiv_plan::{ExecutionKind, NodeOp, PlanNode};
4005
4006 *counter += 1;
4007 let idx = *counter;
4008
4009 match plan {
4010 DfPlan::TableScan(ts) => {
4011 let table_name = ts.table_name.table().to_string();
4012 let row_count = table_row_counts.get(&table_name).copied();
4013 let filters: Vec<String> = ts.filters.iter().map(|e| e.to_string()).collect();
4014 let id = format!("scan-{idx}");
4015 let node = PlanNode::new(&id, format!("Scan {table_name}"), ExecutionKind::Batch)
4016 .with_op(NodeOp::Scan {
4017 table: table_name,
4018 filters,
4019 })
4020 .with_estimated_rows(row_count);
4021 (vec![node], id)
4022 }
4023
4024 DfPlan::Projection(proj) => {
4025 let (mut nodes, input_id) =
4026 df_plan_to_krishiv_nodes(&proj.input, table_row_counts, counter);
4027 let id = format!("proj-{idx}");
4028 let columns: Vec<String> = proj.expr.iter().map(|e| e.to_string()).collect();
4029 nodes.push(
4030 PlanNode::new(&id, "Projection", ExecutionKind::Batch)
4031 .with_op(NodeOp::Project { columns })
4032 .with_inputs([input_id]),
4033 );
4034 (nodes, id)
4035 }
4036
4037 DfPlan::Filter(filter) => {
4038 let (mut nodes, input_id) =
4039 df_plan_to_krishiv_nodes(&filter.input, table_row_counts, counter);
4040 let id = format!("filter-{idx}");
4041 let predicate = filter.predicate.to_string();
4042 nodes.push(
4043 PlanNode::new(&id, "Filter", ExecutionKind::Batch)
4044 .with_op(NodeOp::Filter { predicate })
4045 .with_inputs([input_id]),
4046 );
4047 (nodes, id)
4048 }
4049
4050 DfPlan::Aggregate(agg) => {
4051 let (mut nodes, input_id) =
4052 df_plan_to_krishiv_nodes(&agg.input, table_row_counts, counter);
4053 let id = format!("agg-{idx}");
4054 let group_keys: Vec<String> = agg.group_expr.iter().map(|e| e.to_string()).collect();
4055 nodes.push(
4056 PlanNode::new(&id, "Aggregate", ExecutionKind::Batch)
4057 .with_op(NodeOp::Aggregate { group_keys })
4058 .with_inputs([input_id]),
4059 );
4060 (nodes, id)
4061 }
4062
4063 DfPlan::Join(join) => {
4064 let (mut nodes, left_id) =
4065 df_plan_to_krishiv_nodes(&join.left, table_row_counts, counter);
4066 let (right_nodes, right_id) =
4067 df_plan_to_krishiv_nodes(&join.right, table_row_counts, counter);
4068 nodes.extend(right_nodes);
4069 let id = format!("join-{idx}");
4070 let krishiv_join_type = match join.join_type {
4075 datafusion::common::JoinType::Inner => krishiv_plan::JoinType::Inner,
4076 datafusion::common::JoinType::Left => krishiv_plan::JoinType::Left,
4077 datafusion::common::JoinType::Right => krishiv_plan::JoinType::Right,
4078 datafusion::common::JoinType::Full => krishiv_plan::JoinType::Full,
4079 datafusion::common::JoinType::LeftSemi => krishiv_plan::JoinType::LeftSemi,
4080 datafusion::common::JoinType::RightSemi => krishiv_plan::JoinType::RightSemi,
4081 datafusion::common::JoinType::LeftAnti => krishiv_plan::JoinType::LeftAnti,
4082 datafusion::common::JoinType::RightAnti => krishiv_plan::JoinType::RightAnti,
4083 datafusion::common::JoinType::LeftMark => krishiv_plan::JoinType::LeftSemi,
4087 datafusion::common::JoinType::RightMark => krishiv_plan::JoinType::RightSemi,
4088 };
4089 nodes.push(
4090 PlanNode::new(&id, "Join", ExecutionKind::Batch)
4091 .with_op(NodeOp::Join {
4092 join_type: krishiv_join_type,
4093 })
4094 .with_inputs([left_id, right_id]),
4095 );
4096 (nodes, id)
4097 }
4098
4099 DfPlan::Sort(sort) => {
4100 let (mut nodes, input_id) =
4101 df_plan_to_krishiv_nodes(&sort.input, table_row_counts, counter);
4102 let id = format!("sort-{idx}");
4103 nodes.push(
4104 PlanNode::new(&id, "Sort", ExecutionKind::Batch)
4105 .with_op(NodeOp::Other {
4106 description: format!(
4107 "Sort({})",
4108 sort.expr
4109 .iter()
4110 .map(|e| e.to_string())
4111 .collect::<Vec<_>>()
4112 .join(", ")
4113 ),
4114 })
4115 .with_inputs([input_id]),
4116 );
4117 (nodes, id)
4118 }
4119
4120 DfPlan::Repartition(repart) => {
4121 let (mut nodes, input_id) =
4122 df_plan_to_krishiv_nodes(&repart.input, table_row_counts, counter);
4123 let id = format!("exchange-{idx}");
4124 let partitioning = krishiv_plan::Partitioning::Unpartitioned;
4125 nodes.push(
4126 PlanNode::new(&id, "Exchange", ExecutionKind::Batch)
4127 .with_op(NodeOp::Exchange { partitioning })
4128 .with_inputs([input_id]),
4129 );
4130 (nodes, id)
4131 }
4132
4133 DfPlan::Limit(limit) => {
4134 let (mut nodes, input_id) =
4135 df_plan_to_krishiv_nodes(&limit.input, table_row_counts, counter);
4136 let id = format!("limit-{idx}");
4137 nodes.push(
4138 PlanNode::new(&id, "Limit", ExecutionKind::Batch)
4139 .with_op(NodeOp::Other {
4140 description: format!(
4141 "Limit(skip={:?}, fetch={:?})",
4142 limit.skip.as_ref().map(|e| e.to_string()),
4143 limit.fetch.as_ref().map(|e| e.to_string()),
4144 ),
4145 })
4146 .with_inputs([input_id]),
4147 );
4148 (nodes, id)
4149 }
4150
4151 DfPlan::Union(union) if union.inputs.len() == 1 => {
4152 if let Some(input) = union.inputs.first() {
4153 df_plan_to_krishiv_nodes(input, table_row_counts, counter)
4154 } else {
4155 (Vec::new(), String::new())
4156 }
4157 }
4158 DfPlan::Union(union) => {
4159 let mut all_nodes = Vec::new();
4160 let mut input_ids = Vec::new();
4161 for input in &union.inputs {
4162 let (sub_nodes, sub_id) =
4163 df_plan_to_krishiv_nodes(input, table_row_counts, counter);
4164 all_nodes.extend(sub_nodes);
4165 input_ids.push(sub_id);
4166 }
4167 let id = format!("union-{idx}");
4168 all_nodes.push(
4169 PlanNode::new(&id, "Union", ExecutionKind::Batch)
4170 .with_op(NodeOp::Other {
4171 description: "Union".to_string(),
4172 })
4173 .with_inputs(input_ids),
4174 );
4175 (all_nodes, id)
4176 }
4177
4178 DfPlan::SubqueryAlias(alias) => {
4179 df_plan_to_krishiv_nodes(&alias.input, table_row_counts, counter)
4181 }
4182
4183 DfPlan::Values(_) => {
4184 let id = format!("values-{idx}");
4185 let node = PlanNode::new(&id, "Values", ExecutionKind::Batch).with_op(NodeOp::Other {
4186 description: "Values".to_string(),
4187 });
4188 (vec![node], id)
4189 }
4190
4191 DfPlan::Extension(_) => {
4192 let id = format!("ext-{idx}");
4193 let label = plan.to_string();
4194 let node = PlanNode::new(&id, label.clone(), ExecutionKind::Batch)
4195 .with_op(NodeOp::Other { description: label });
4196 (vec![node], id)
4197 }
4198
4199 DfPlan::EmptyRelation(_) => {
4200 let id = format!("empty-{idx}");
4201 let node =
4202 PlanNode::new(&id, "EmptyRelation", ExecutionKind::Batch).with_op(NodeOp::Other {
4203 description: "EmptyRelation".to_string(),
4204 });
4205 (vec![node], id)
4206 }
4207
4208 _ => {
4210 let id = format!("df-{idx}");
4211 let label = plan.to_string();
4212 let node = PlanNode::new(&id, label.clone(), ExecutionKind::Batch)
4213 .with_op(NodeOp::Other { description: label });
4214 (vec![node], id)
4215 }
4216 }
4217}
4218
4219#[derive(Clone)]
4221pub struct SqlDataFrame {
4222 name: String,
4223 query: Option<String>,
4224 query_text: Option<String>,
4226 execution_kind: ExecutionKind,
4227 dataframe: DataFusionDataFrame,
4228 shuffle_partitions: Option<u32>,
4229 context: SessionContext,
4231 table_row_counts: Arc<std::sync::RwLock<HashMap<String, u64>>>,
4235}
4236
4237impl fmt::Debug for SqlDataFrame {
4238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4239 f.debug_struct("SqlDataFrame")
4240 .field("name", &self.name)
4241 .field("query", &self.query)
4242 .field("shuffle_partitions", &self.shuffle_partitions)
4243 .finish_non_exhaustive()
4244 }
4245}
4246
4247impl SqlDataFrame {
4248 fn new(
4249 name: impl Into<String>,
4250 dataframe: DataFusionDataFrame,
4251 table_row_counts: Arc<std::sync::RwLock<HashMap<String, u64>>>,
4252 ) -> Self {
4253 Self {
4254 name: name.into(),
4255 query: None,
4256 query_text: None,
4257 execution_kind: ExecutionKind::Batch,
4258 dataframe,
4259 shuffle_partitions: None,
4260 context: SessionContext::default(),
4261 table_row_counts,
4262 }
4263 }
4264
4265 pub(crate) fn with_context(mut self, context: SessionContext) -> Self {
4267 self.context = context;
4268 self
4269 }
4270
4271 fn with_query(mut self, query: impl Into<String>) -> Self {
4272 let q = query.into();
4273 self.query_text = Some(q.clone());
4274 self.query = Some(q);
4275 self
4276 }
4277
4278 fn with_execution_kind(mut self, kind: ExecutionKind) -> Self {
4279 self.execution_kind = kind;
4280 self
4281 }
4282
4283 fn with_shuffle_partitions(mut self, n: Option<u32>) -> Self {
4284 self.shuffle_partitions = n;
4285 self
4286 }
4287
4288 pub fn query(&self) -> Option<&str> {
4290 self.query.as_deref()
4291 }
4292
4293 pub fn arrow_schema(&self) -> arrow::datatypes::SchemaRef {
4299 std::sync::Arc::new(self.dataframe.schema().as_arrow().clone())
4300 }
4301
4302 fn with_new_dataframe(&self, df: DataFusionDataFrame, tag: &str) -> Self {
4306 Self {
4307 name: format!("{}-{}", self.name, tag),
4308 query: None,
4309 query_text: None,
4310 execution_kind: self.execution_kind,
4311 dataframe: df,
4312 shuffle_partitions: self.shuffle_partitions,
4313 context: self.context.clone(),
4314 table_row_counts: self.table_row_counts.clone(),
4315 }
4316 }
4317
4318 pub fn krishiv_logical_plan(&self) -> LogicalPlan {
4327 let df_plan = self.dataframe.logical_plan();
4328 let counts = self
4329 .table_row_counts
4330 .read()
4331 .unwrap_or_else(|e| e.into_inner());
4332 let mut counter = 0usize;
4333 let (nodes, _root_id) = df_plan_to_krishiv_nodes(df_plan, &counts, &mut counter);
4334
4335 let mut plan = LogicalPlan::new(self.name.clone(), self.execution_kind);
4336 for node in nodes {
4337 plan = plan.with_node(node);
4338 }
4339
4340 let optimizer = krishiv_plan::optimizer::default_logical_optimizer();
4345 let fallback = plan.clone();
4346 match optimizer.optimize(plan) {
4347 Ok(result) => result.plan,
4348 Err(error) => {
4349 tracing::warn!(
4350 plan = %self.name,
4351 %error,
4352 "logical optimizer failed; using unoptimized plan"
4353 );
4354 fallback
4355 }
4356 }
4357 }
4358
4359 pub fn explain_logical(&self) -> String {
4361 self.dataframe.logical_plan().to_string()
4362 }
4363
4364 pub async fn explain(&self) -> SqlResult<String> {
4366 let batches = self
4367 .dataframe
4368 .clone()
4369 .explain(false, false)?
4370 .collect()
4371 .await?;
4372 pretty_batches(&batches)
4373 }
4374
4375 pub async fn explain_analyze(&self) -> SqlResult<String> {
4390 let batches = self
4391 .dataframe
4392 .clone()
4393 .explain(false, true)?
4394 .collect()
4395 .await?;
4396 pretty_batches(&batches)
4397 }
4398
4399 pub fn collect(
4404 &self,
4405 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = SqlResult<Vec<RecordBatch>>> + Send + '_>>
4406 {
4407 Box::pin(async move { Ok(self.dataframe.clone().collect().await?) })
4408 }
4409
4410 pub fn execute_stream(
4416 &self,
4417 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = SqlResult<SqlStream>> + Send + '_>>
4418 {
4419 Box::pin(self.execute_stream_boxed_body())
4420 }
4421
4422 async fn execute_stream_boxed_body(&self) -> SqlResult<SqlStream> {
4423 Ok(self.execute_stream_with_schema_boxed_body().await?.1)
4424 }
4425
4426 pub fn execute_stream_with_schema(
4441 &self,
4442 ) -> futures::future::BoxFuture<'_, SqlResult<(SchemaRef, SqlStream)>> {
4443 Box::pin(self.execute_stream_with_schema_boxed_body())
4444 }
4445
4446 async fn execute_stream_with_schema_boxed_body(&self) -> SqlResult<(SchemaRef, SqlStream)> {
4447 let df_stream = self.dataframe.clone().execute_stream().await?;
4448 let schema = df_stream.schema();
4449 use futures::StreamExt;
4450 let mapped = df_stream.map(|res| {
4451 res.map_err(|e| SqlError::DataFusion {
4452 message: e.to_string(),
4453 })
4454 });
4455 Ok((schema, Box::pin(mapped)))
4456 }
4457
4458 pub fn collect_with_stats(
4466 &self,
4467 ) -> futures::future::BoxFuture<'_, SqlResult<(Vec<RecordBatch>, SqlExecutionStats)>> {
4468 Box::pin(self.collect_with_stats_boxed_body())
4469 }
4470
4471 async fn collect_with_stats_boxed_body(
4472 &self,
4473 ) -> SqlResult<(Vec<RecordBatch>, SqlExecutionStats)> {
4474 use datafusion::physical_plan::collect as df_collect;
4475
4476 let df = self.dataframe.clone();
4477 let task_ctx = df.task_ctx();
4478 let physical_plan = df.create_physical_plan().await?;
4479
4480 let batches = df_collect(physical_plan.clone(), task_ctx.into()).await?;
4481
4482 let mut output_rows: u64 = batches.iter().map(|b| b.num_rows() as u64).sum();
4483 let mut cpu_nanos: u64 = 0;
4484
4485 if let Some(metrics) = physical_plan.metrics() {
4486 if let Some(v) = metrics.output_rows() {
4487 output_rows = v as u64;
4488 }
4489 if let Some(t) = metrics.elapsed_compute() {
4490 cpu_nanos = t as u64;
4491 }
4492 }
4493
4494 let (spill_bytes, spill_count) = aggregate_spill_metrics(physical_plan.as_ref());
4495
4496 Ok((
4497 batches,
4498 SqlExecutionStats {
4499 output_rows,
4500 cpu_nanos,
4501 spill_bytes,
4502 spill_count,
4503 },
4504 ))
4505 }
4506
4507 pub fn execute_stream_with_stats(
4517 &self,
4518 ) -> futures::future::BoxFuture<'_, SqlResult<(SqlStream, SqlStatsHandle)>> {
4519 Box::pin(self.execute_stream_with_stats_boxed_body())
4520 }
4521
4522 async fn execute_stream_with_stats_boxed_body(&self) -> SqlResult<(SqlStream, SqlStatsHandle)> {
4523 use futures::StreamExt;
4524
4525 let df = self.dataframe.clone();
4526 let task_ctx = df.task_ctx();
4527 let physical_plan = df.create_physical_plan().await?;
4528 let df_stream = datafusion::physical_plan::execute_stream(
4529 physical_plan.clone(),
4530 std::sync::Arc::new(task_ctx),
4531 )?;
4532 let mapped = df_stream.map(|res| {
4533 res.map_err(|e| SqlError::DataFusion {
4534 message: e.to_string(),
4535 })
4536 });
4537 Ok((
4538 Box::pin(mapped),
4539 SqlStatsHandle {
4540 plan: physical_plan,
4541 },
4542 ))
4543 }
4544}
4545
4546pub struct SqlStatsHandle {
4549 plan: std::sync::Arc<dyn datafusion::physical_plan::ExecutionPlan>,
4550}
4551
4552impl SqlStatsHandle {
4553 pub fn stats(&self) -> SqlExecutionStats {
4558 let mut output_rows: u64 = 0;
4559 let mut cpu_nanos: u64 = 0;
4560 if let Some(metrics) = self.plan.metrics() {
4561 if let Some(v) = metrics.output_rows() {
4562 output_rows = v as u64;
4563 }
4564 if let Some(t) = metrics.elapsed_compute() {
4565 cpu_nanos = t as u64;
4566 }
4567 }
4568 let (spill_bytes, spill_count) = aggregate_spill_metrics(self.plan.as_ref());
4569 SqlExecutionStats {
4570 output_rows,
4571 cpu_nanos,
4572 spill_bytes,
4573 spill_count,
4574 }
4575 }
4576}
4577
4578fn aggregate_spill_metrics(plan: &dyn datafusion::physical_plan::ExecutionPlan) -> (u64, u64) {
4585 let mut spill_bytes: u64 = 0;
4586 let mut spill_count: u64 = 0;
4587 if let Some(metrics) = plan.metrics() {
4588 if let Some(bytes) = metrics.spilled_bytes() {
4589 spill_bytes = spill_bytes.saturating_add(bytes as u64);
4590 }
4591 if let Some(count) = metrics.spill_count() {
4592 spill_count = spill_count.saturating_add(count as u64);
4593 }
4594 }
4595 for child in plan.children() {
4596 let (child_bytes, child_count) = aggregate_spill_metrics(child.as_ref());
4597 spill_bytes = spill_bytes.saturating_add(child_bytes);
4598 spill_count = spill_count.saturating_add(child_count);
4599 }
4600 (spill_bytes, spill_count)
4601}
4602
4603#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4605pub struct SqlExecutionStats {
4606 pub output_rows: u64,
4607 pub cpu_nanos: u64,
4608 pub spill_bytes: u64,
4610 pub spill_count: u64,
4612}
4613
4614fn top_level_alias_index(expression: &str) -> Option<usize> {
4615 let bytes = expression.as_bytes();
4616 let mut depth = 0usize;
4617 let mut single_quoted = false;
4618 let mut double_quoted = false;
4619 let mut candidate = None;
4620 let mut index = 0usize;
4621 while index < bytes.len() {
4622 let Some(&byte) = bytes.get(index) else {
4623 break;
4624 };
4625 match byte {
4626 b'\'' if !double_quoted => {
4627 if single_quoted && bytes.get(index + 1) == Some(&b'\'') {
4628 index += 2;
4629 continue;
4630 }
4631 single_quoted = !single_quoted;
4632 }
4633 b'"' if !single_quoted => {
4634 if double_quoted && bytes.get(index + 1) == Some(&b'"') {
4635 index += 2;
4636 continue;
4637 }
4638 double_quoted = !double_quoted;
4639 }
4640 b'(' if !single_quoted && !double_quoted => depth += 1,
4641 b')' if !single_quoted && !double_quoted => depth = depth.saturating_sub(1),
4642 b' ' if depth == 0
4643 && !single_quoted
4644 && !double_quoted
4645 && bytes
4646 .get(index..index + 4)
4647 .is_some_and(|slice| slice.eq_ignore_ascii_case(b" AS ")) =>
4648 {
4649 candidate = Some(index);
4650 index += 3;
4651 }
4652 _ => {}
4653 }
4654 index += 1;
4655 }
4656 candidate
4657}
4658
4659fn parse_dataframe_expression(
4660 dataframe: &datafusion::dataframe::DataFrame,
4661 expression: &str,
4662) -> SqlResult<datafusion::logical_expr::Expr> {
4663 if let Some(index) = top_level_alias_index(expression) {
4664 let (body, alias) = expression.split_at(index);
4665 let alias = alias[4..].trim();
4666 if !alias.is_empty() {
4667 let alias = alias
4668 .strip_prefix('"')
4669 .and_then(|value| value.strip_suffix('"'))
4670 .unwrap_or(alias)
4671 .replace("\"\"", "\"");
4672 return Ok(dataframe.parse_sql_expr(body.trim())?.alias(alias));
4673 }
4674 }
4675 dataframe.parse_sql_expr(expression).map_err(Into::into)
4676}
4677
4678pub fn parse_public_expression(sql: &str) -> SqlResult<krishiv_plan::expression::Expr> {
4680 let dialect = GenericDialect {};
4681 let mut parser =
4682 Parser::new(&dialect)
4683 .try_with_sql(sql)
4684 .map_err(|error| SqlError::Unsupported {
4685 feature: format!("public expression parse: {error}"),
4686 })?;
4687 let expression = parser.parse_expr().map_err(|error| SqlError::Unsupported {
4688 feature: format!("public expression parse: {error}"),
4689 })?;
4690 sqlparser_expression_to_public(&expression)
4691}
4692
4693fn sqlparser_expression_to_public(
4694 expression: &datafusion::sql::sqlparser::ast::Expr,
4695) -> SqlResult<krishiv_plan::expression::Expr> {
4696 use datafusion::sql::sqlparser::ast::{BinaryOperator as SqlOperator, Expr as SqlExpr, Value};
4697 use krishiv_plan::expression::{BinaryOperator, Expr, ScalarValue};
4698
4699 Ok(match expression {
4700 SqlExpr::Identifier(identifier) => Expr::Column {
4701 path: vec![identifier.value.clone()],
4702 },
4703 SqlExpr::CompoundIdentifier(identifiers) => Expr::Column {
4704 path: identifiers
4705 .iter()
4706 .map(|identifier| identifier.value.clone())
4707 .collect(),
4708 },
4709 SqlExpr::Nested(expression) => sqlparser_expression_to_public(expression)?,
4710 SqlExpr::IsNull(expression) => Expr::IsNull {
4711 expression: Box::new(sqlparser_expression_to_public(expression)?),
4712 negated: false,
4713 },
4714 SqlExpr::IsNotNull(expression) => Expr::IsNull {
4715 expression: Box::new(sqlparser_expression_to_public(expression)?),
4716 negated: true,
4717 },
4718 SqlExpr::BinaryOp { left, op, right } => Expr::Binary {
4719 left: Box::new(sqlparser_expression_to_public(left)?),
4720 op: match op {
4721 SqlOperator::Eq => BinaryOperator::Eq,
4722 SqlOperator::NotEq => BinaryOperator::NotEq,
4723 SqlOperator::Gt => BinaryOperator::Gt,
4724 SqlOperator::GtEq => BinaryOperator::GtEq,
4725 SqlOperator::Lt => BinaryOperator::Lt,
4726 SqlOperator::LtEq => BinaryOperator::LtEq,
4727 SqlOperator::And => BinaryOperator::And,
4728 SqlOperator::Or => BinaryOperator::Or,
4729 SqlOperator::Plus => BinaryOperator::Plus,
4730 SqlOperator::Minus => BinaryOperator::Minus,
4731 SqlOperator::Multiply => BinaryOperator::Multiply,
4732 SqlOperator::Divide => BinaryOperator::Divide,
4733 other => {
4734 return Err(SqlError::Unsupported {
4735 feature: format!("public expression operator {other}"),
4736 });
4737 }
4738 },
4739 right: Box::new(sqlparser_expression_to_public(right)?),
4740 },
4741 SqlExpr::Value(value) => Expr::Literal {
4742 value: match &value.value {
4743 Value::Null => ScalarValue::Null,
4744 Value::Boolean(value) => ScalarValue::Boolean(*value),
4745 Value::SingleQuotedString(value) => ScalarValue::Utf8(value.clone()),
4746 Value::Number(value, _)
4747 if value.contains('.') || value.contains('e') || value.contains('E') =>
4748 {
4749 ScalarValue::float64(value.parse::<f64>().map_err(|error| {
4750 SqlError::Unsupported {
4751 feature: format!("numeric expression literal: {error}"),
4752 }
4753 })?)
4754 }
4755 Value::Number(value, _) => {
4756 ScalarValue::Int64(value.parse::<i64>().map_err(|error| {
4757 SqlError::Unsupported {
4758 feature: format!("integer expression literal: {error}"),
4759 }
4760 })?)
4761 }
4762 other => {
4763 return Err(SqlError::Unsupported {
4764 feature: format!("public expression literal {other}"),
4765 });
4766 }
4767 },
4768 },
4769 other => {
4770 return Err(SqlError::Unsupported {
4771 feature: format!("public expression node {other}"),
4772 });
4773 }
4774 })
4775}
4776
4777fn public_data_type_to_arrow(
4778 data_type: &krishiv_plan::expression::ExprDataType,
4779) -> arrow::datatypes::DataType {
4780 use arrow::datatypes::{DataType, Field, IntervalUnit, TimeUnit};
4781 use krishiv_plan::expression::{ExprDataType, IntervalUnit as PublicIntervalUnit};
4782
4783 match data_type {
4784 ExprDataType::Null => DataType::Null,
4785 ExprDataType::Boolean => DataType::Boolean,
4786 ExprDataType::Int64 => DataType::Int64,
4787 ExprDataType::UInt64 => DataType::UInt64,
4788 ExprDataType::Float64 => DataType::Float64,
4789 ExprDataType::Utf8 => DataType::Utf8,
4790 ExprDataType::Binary => DataType::Binary,
4791 ExprDataType::Decimal128 { precision, scale } => DataType::Decimal128(*precision, *scale),
4792 ExprDataType::Date32 => DataType::Date32,
4793 ExprDataType::Timestamp { unit, timezone } => DataType::Timestamp(
4794 match unit {
4795 krishiv_plan::expression::TimeUnit::Second => TimeUnit::Second,
4796 krishiv_plan::expression::TimeUnit::Millisecond => TimeUnit::Millisecond,
4797 krishiv_plan::expression::TimeUnit::Microsecond => TimeUnit::Microsecond,
4798 krishiv_plan::expression::TimeUnit::Nanosecond => TimeUnit::Nanosecond,
4799 },
4800 timezone.clone().map(Into::into),
4801 ),
4802 ExprDataType::Interval { unit } => DataType::Interval(match unit {
4803 PublicIntervalUnit::YearMonth => IntervalUnit::YearMonth,
4804 PublicIntervalUnit::DayTime => IntervalUnit::DayTime,
4805 PublicIntervalUnit::MonthDayNano => IntervalUnit::MonthDayNano,
4806 }),
4807 ExprDataType::List(element) => DataType::List(Arc::new(Field::new(
4808 "item",
4809 public_data_type_to_arrow(element),
4810 true,
4811 ))),
4812 ExprDataType::Map { key, value } => DataType::Map(
4813 Arc::new(Field::new(
4814 "entries",
4815 DataType::Struct(
4816 vec![
4817 Arc::new(Field::new("key", public_data_type_to_arrow(key), false)),
4818 Arc::new(Field::new("value", public_data_type_to_arrow(value), true)),
4819 ]
4820 .into(),
4821 ),
4822 false,
4823 )),
4824 false,
4825 ),
4826 ExprDataType::Struct(fields) => DataType::Struct(
4827 fields
4828 .iter()
4829 .map(|field| {
4830 Arc::new(Field::new(
4831 &field.name,
4832 public_data_type_to_arrow(&field.data_type),
4833 field.nullable,
4834 ))
4835 })
4836 .collect::<Vec<_>>()
4837 .into(),
4838 ),
4839 ExprDataType::Variant => DataType::Utf8,
4844 }
4845}
4846
4847fn public_scalar_to_datafusion(
4848 value: &krishiv_plan::expression::ScalarValue,
4849) -> Option<datafusion::common::ScalarValue> {
4850 use datafusion::common::ScalarValue;
4851 use krishiv_plan::expression::{ScalarValue as PublicScalar, TimeUnit};
4852
4853 Some(match value {
4854 PublicScalar::Null => ScalarValue::Null,
4855 PublicScalar::Boolean(value) => ScalarValue::Boolean(Some(*value)),
4856 PublicScalar::Int64(value) => ScalarValue::Int64(Some(*value)),
4857 PublicScalar::UInt64(value) => ScalarValue::UInt64(Some(*value)),
4858 PublicScalar::Float64(bits) => ScalarValue::Float64(Some(f64::from_bits(*bits))),
4859 PublicScalar::Utf8(value) => ScalarValue::Utf8(Some(value.clone())),
4860 PublicScalar::Binary(value) => ScalarValue::Binary(Some(value.clone())),
4861 PublicScalar::Decimal128 {
4862 value,
4863 precision,
4864 scale,
4865 } => ScalarValue::Decimal128(Some(*value), *precision, *scale),
4866 PublicScalar::Date32(value) => ScalarValue::Date32(Some(*value)),
4867 PublicScalar::Timestamp {
4868 value,
4869 unit,
4870 timezone,
4871 } => {
4872 let timezone = timezone.clone().map(Into::into);
4873 match unit {
4874 TimeUnit::Second => ScalarValue::TimestampSecond(Some(*value), timezone),
4875 TimeUnit::Millisecond => ScalarValue::TimestampMillisecond(Some(*value), timezone),
4876 TimeUnit::Microsecond => ScalarValue::TimestampMicrosecond(Some(*value), timezone),
4877 TimeUnit::Nanosecond => ScalarValue::TimestampNanosecond(Some(*value), timezone),
4878 }
4879 }
4880 PublicScalar::Interval { .. } => return None,
4881 })
4882}
4883
4884fn lower_public_expression(
4890 dataframe: &datafusion::dataframe::DataFrame,
4891 expression: &krishiv_plan::expression::Expr,
4892) -> SqlResult<datafusion::logical_expr::Expr> {
4893 expression
4894 .validate()
4895 .map_err(|error| SqlError::Unsupported {
4896 feature: format!("invalid public expression: {error}"),
4897 })?;
4898 use datafusion::logical_expr::{Expr as DataFusionExpr, Operator, binary_expr, cast, try_cast};
4899 use krishiv_plan::expression::{BinaryOperator, Expr};
4900
4901 Ok(match expression {
4902 Expr::Column { path } if path.len() == 1 => {
4903 datafusion::prelude::col(path.first().map(String::as_str).unwrap_or(""))
4904 }
4905 Expr::Column { .. } => parse_dataframe_expression(dataframe, &expression.to_sql())?,
4906 Expr::Literal { value } => match public_scalar_to_datafusion(value) {
4907 Some(value) => DataFusionExpr::Literal(value, None),
4908 None => parse_dataframe_expression(dataframe, &expression.to_sql())?,
4909 },
4910 Expr::Alias { expression, name } => {
4911 lower_public_expression(dataframe, expression)?.alias(name)
4912 }
4913 Expr::Binary { left, op, right } => binary_expr(
4914 lower_public_expression(dataframe, left)?,
4915 match op {
4916 BinaryOperator::Eq => Operator::Eq,
4917 BinaryOperator::NotEq => Operator::NotEq,
4918 BinaryOperator::Gt => Operator::Gt,
4919 BinaryOperator::GtEq => Operator::GtEq,
4920 BinaryOperator::Lt => Operator::Lt,
4921 BinaryOperator::LtEq => Operator::LtEq,
4922 BinaryOperator::And => Operator::And,
4923 BinaryOperator::Or => Operator::Or,
4924 BinaryOperator::Plus => Operator::Plus,
4925 BinaryOperator::Minus => Operator::Minus,
4926 BinaryOperator::Multiply => Operator::Multiply,
4927 BinaryOperator::Divide => Operator::Divide,
4928 },
4929 lower_public_expression(dataframe, right)?,
4930 ),
4931 Expr::IsNull {
4932 expression,
4933 negated,
4934 } => {
4935 let expression = lower_public_expression(dataframe, expression)?;
4936 if *negated {
4937 expression.is_not_null()
4938 } else {
4939 expression.is_null()
4940 }
4941 }
4942 Expr::Cast {
4943 expression,
4944 data_type,
4945 safe,
4946 } => {
4947 let expression = lower_public_expression(dataframe, expression)?;
4948 let data_type = public_data_type_to_arrow(data_type);
4949 if *safe {
4950 try_cast(expression, data_type)
4951 } else {
4952 cast(expression, data_type)
4953 }
4954 }
4955 Expr::Sort { .. } => {
4956 return Err(SqlError::Unsupported {
4957 feature: "standalone sort expressions are only valid inside windows or order_by"
4958 .into(),
4959 });
4960 }
4961 Expr::Aggregate { .. }
4962 | Expr::Function { .. }
4963 | Expr::Window { .. }
4964 | Expr::RawSql { .. } => parse_dataframe_expression(dataframe, &expression.to_sql())?,
4965 })
4966}
4967
4968fn sql_dataframe<'a>(
4969 dataframe: &'a dyn KrishivDataFrameOps,
4970 operation: &str,
4971) -> SqlResult<&'a SqlDataFrame> {
4972 dataframe
4973 .as_any()
4974 .downcast_ref::<SqlDataFrame>()
4975 .ok_or_else(|| SqlError::DataFusion {
4976 message: format!("right DataFrame must be SqlDataFrame for {operation}"),
4977 })
4978}
4979
4980#[async_trait::async_trait]
4981impl KrishivDataFrameOps for SqlDataFrame {
4982 async fn collect(&self) -> SqlResult<Vec<RecordBatch>> {
4983 SqlDataFrame::collect(self).await
4984 }
4985 async fn collect_with_stats(&self) -> SqlResult<(Vec<RecordBatch>, SqlExecutionStats)> {
4986 SqlDataFrame::collect_with_stats(self).await
4987 }
4988 async fn explain_analyze(&self) -> SqlResult<String> {
4989 SqlDataFrame::explain_analyze(self).await
4990 }
4991
4992 async fn explain(&self) -> SqlResult<String> {
4993 SqlDataFrame::explain(self).await
4994 }
4995 fn explain_logical(&self) -> String {
4996 SqlDataFrame::explain_logical(self)
4997 }
4998 fn krishiv_logical_plan(&self) -> LogicalPlan {
4999 let label = self.dataframe.logical_plan().to_string();
5000 let mut plan = LogicalPlan::new(self.name.clone(), ExecutionKind::Batch).with_node(
5001 PlanNode::new("datafusion-logical", label, ExecutionKind::Batch),
5002 );
5003 if let Some(n) = self.shuffle_partitions {
5004 plan = plan.with_shuffle_partitions(Some(n));
5005 }
5006 plan
5007 }
5008 fn query(&self) -> Option<&str> {
5009 SqlDataFrame::query(self)
5010 }
5011 fn to_sql(&self) -> SqlResult<String> {
5012 match datafusion::sql::unparser::plan_to_sql(self.dataframe.logical_plan()) {
5015 Ok(statement) => Ok(statement.to_string()),
5016 Err(err) => self
5017 .query()
5018 .map(str::to_string)
5019 .ok_or_else(|| SqlError::Unsupported {
5020 feature: format!("cannot render DataFrame plan as SQL: {err}"),
5021 }),
5022 }
5023 }
5024 async fn execute_stream(&self) -> SqlResult<SqlStream> {
5025 SqlDataFrame::execute_stream(self).await
5026 }
5027
5028 fn schema(&self) -> SchemaRef {
5031 SchemaRef::from(self.dataframe.schema().clone())
5032 }
5033
5034 async fn select(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5035 let df = self.dataframe.clone().select_columns(columns)?;
5036 Ok(Box::new(self.with_new_dataframe(df, "select")))
5037 }
5038
5039 async fn select_exprs(
5040 &self,
5041 expressions: &[&krishiv_plan::expression::Expr],
5042 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5043 let expressions = expressions
5044 .iter()
5045 .map(|expression| lower_public_expression(&self.dataframe, expression))
5046 .collect::<Result<Vec<_>, _>>()?;
5047 let df = self.dataframe.clone().select(expressions)?;
5048 Ok(Box::new(self.with_new_dataframe(df, "select_exprs")))
5049 }
5050
5051 async fn unnest_columns(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5052 let df = self.dataframe.clone().unnest_columns(columns)?;
5053 Ok(Box::new(self.with_new_dataframe(df, "unnest")))
5054 }
5055
5056 async fn aggregate(
5057 &self,
5058 group_exprs: &[&krishiv_plan::expression::Expr],
5059 aggregate_exprs: &[&krishiv_plan::expression::Expr],
5060 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5061 if aggregate_exprs.is_empty() {
5062 return Err(SqlError::Unsupported {
5063 feature: "aggregate requires at least one aggregate expression".into(),
5064 });
5065 }
5066 let group_exprs = group_exprs
5067 .iter()
5068 .map(|expression| lower_public_expression(&self.dataframe, expression))
5069 .collect::<Result<Vec<_>, _>>()?;
5070 let aggregate_exprs = aggregate_exprs
5071 .iter()
5072 .map(|expression| lower_public_expression(&self.dataframe, expression))
5073 .collect::<Result<Vec<_>, _>>()?;
5074 let df = self
5075 .dataframe
5076 .clone()
5077 .aggregate(group_exprs, aggregate_exprs)?;
5078 Ok(Box::new(self.with_new_dataframe(df, "aggregate")))
5079 }
5080
5081 async fn aggregate_grouping(
5082 &self,
5083 grouping: GroupingMode<'_>,
5084 aggregate_exprs: &[&krishiv_plan::expression::Expr],
5085 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5086 if aggregate_exprs.is_empty() {
5087 return Err(SqlError::Unsupported {
5088 feature: "grouping aggregation requires at least one aggregate expression".into(),
5089 });
5090 }
5091 let lower = |expression: &&krishiv_plan::expression::Expr| {
5092 lower_public_expression(&self.dataframe, expression)
5093 };
5094 let group = match grouping {
5095 GroupingMode::Sets(sets) => datafusion::logical_expr::grouping_set(
5096 sets.into_iter()
5097 .map(|set| set.iter().map(lower).collect::<Result<Vec<_>, _>>())
5098 .collect::<Result<Vec<_>, _>>()?,
5099 ),
5100 GroupingMode::Cube(expressions) => datafusion::logical_expr::cube(
5101 expressions
5102 .iter()
5103 .map(lower)
5104 .collect::<Result<Vec<_>, _>>()?,
5105 ),
5106 GroupingMode::Rollup(expressions) => datafusion::logical_expr::rollup(
5107 expressions
5108 .iter()
5109 .map(lower)
5110 .collect::<Result<Vec<_>, _>>()?,
5111 ),
5112 };
5113 let aggregates = aggregate_exprs
5114 .iter()
5115 .map(lower)
5116 .collect::<Result<Vec<_>, _>>()?;
5117 let df = self.dataframe.clone().aggregate(vec![group], aggregates)?;
5118 Ok(Box::new(self.with_new_dataframe(df, "aggregate_grouping")))
5119 }
5120
5121 async fn pivot(
5122 &self,
5123 group_exprs: &[&krishiv_plan::expression::Expr],
5124 pivot_column: &krishiv_plan::expression::Expr,
5125 aggregate_expr: &krishiv_plan::expression::Expr,
5126 values: &[(krishiv_plan::expression::ScalarValue, String)],
5127 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5128 use krishiv_plan::expression::Expr as PublicExpr;
5129 let (function, input, distinct) = match aggregate_expr {
5130 PublicExpr::Aggregate {
5131 function,
5132 expression: Some(input),
5133 distinct,
5134 } => (*function, input.as_ref(), *distinct),
5135 _ => {
5136 return Err(SqlError::Unsupported {
5137 feature: "pivot requires an aggregate expression with one input".into(),
5138 });
5139 }
5140 };
5141 if values.is_empty() {
5142 return Err(SqlError::Unsupported {
5143 feature: "pivot requires at least one value".into(),
5144 });
5145 }
5146 let group_exprs = group_exprs
5147 .iter()
5148 .map(|expression| lower_public_expression(&self.dataframe, expression))
5149 .collect::<Result<Vec<_>, _>>()?;
5150 let aggregates = values
5151 .iter()
5152 .map(|(value, alias)| {
5153 let conditional = PublicExpr::raw(format!(
5154 "CASE WHEN {} = {} THEN {} END",
5155 pivot_column.to_sql(),
5156 value.to_sql_literal(),
5157 input.to_sql()
5158 ));
5159 let aggregate = PublicExpr::Aggregate {
5160 function,
5161 expression: Some(Box::new(conditional)),
5162 distinct,
5163 }
5164 .alias(alias);
5165 lower_public_expression(&self.dataframe, &aggregate)
5166 })
5167 .collect::<Result<Vec<_>, _>>()?;
5168 let dataframe = self.dataframe.clone().aggregate(group_exprs, aggregates)?;
5169 Ok(Box::new(self.with_new_dataframe(dataframe, "pivot")))
5170 }
5171
5172 async fn unpivot(
5173 &self,
5174 columns: &[&str],
5175 name_column: &str,
5176 value_column: &str,
5177 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5178 if columns.is_empty() {
5179 return Err(SqlError::Unsupported {
5180 feature: "unpivot requires at least one column".into(),
5181 });
5182 }
5183 let retained = self
5184 .dataframe
5185 .schema()
5186 .fields()
5187 .iter()
5188 .map(|field| field.name().as_str())
5189 .filter(|name| !columns.contains(name))
5190 .collect::<Vec<_>>();
5191 let mut branches = Vec::with_capacity(columns.len());
5192 for column in columns {
5193 let mut expressions = retained
5194 .iter()
5195 .map(|name| datafusion::logical_expr::col(*name))
5196 .collect::<Vec<_>>();
5197 expressions
5198 .push(datafusion::logical_expr::lit((*column).to_owned()).alias(name_column));
5199 expressions.push(datafusion::logical_expr::col(*column).alias(value_column));
5200 branches.push(self.dataframe.clone().select(expressions)?);
5201 }
5202 let mut branches = branches.into_iter();
5203 let Some(mut dataframe) = branches.next() else {
5204 return Err(SqlError::Unsupported {
5205 feature: "unpivot requires at least one branch".into(),
5206 });
5207 };
5208 for branch in branches {
5209 dataframe = dataframe.union(branch)?;
5210 }
5211 Ok(Box::new(self.with_new_dataframe(dataframe, "unpivot")))
5212 }
5213
5214 async fn filter(&self, predicate: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5215 let expr = self.dataframe.parse_sql_expr(predicate)?;
5216 let df = self.dataframe.clone().filter(expr)?;
5217 Ok(Box::new(self.with_new_dataframe(df, "filter")))
5218 }
5219
5220 async fn filter_expr(
5221 &self,
5222 predicate: &krishiv_plan::expression::Expr,
5223 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5224 let expr = lower_public_expression(&self.dataframe, predicate)?;
5225 let df = self.dataframe.clone().filter(expr)?;
5226 Ok(Box::new(self.with_new_dataframe(df, "filter_expr")))
5227 }
5228
5229 async fn limit(&self, n: usize) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5230 let df = self.dataframe.clone().limit(0, Some(n))?;
5231 Ok(Box::new(self.with_new_dataframe(df, "limit")))
5232 }
5233
5234 async fn distinct(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5235 let df = self.dataframe.clone().distinct()?;
5236 Ok(Box::new(self.with_new_dataframe(df, "distinct")))
5237 }
5238
5239 async fn drop_nulls(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5240 let columns = if columns.is_empty() {
5241 self.dataframe
5242 .schema()
5243 .fields()
5244 .iter()
5245 .map(|field| field.name().as_str())
5246 .collect::<Vec<_>>()
5247 } else {
5248 columns.to_vec()
5249 };
5250 let mut predicate: Option<datafusion::logical_expr::Expr> = None;
5251 for column in columns {
5252 let next = datafusion::logical_expr::col(column).is_not_null();
5253 predicate = Some(match predicate {
5254 Some(current) => current.and(next),
5255 None => next,
5256 });
5257 }
5258 let df = match predicate {
5259 Some(predicate) => self.dataframe.clone().filter(predicate)?,
5260 None => self.dataframe.clone(),
5261 };
5262 Ok(Box::new(self.with_new_dataframe(df, "drop_nulls")))
5263 }
5264
5265 async fn sample(&self, fraction: f64) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5266 if !(0.0..=1.0).contains(&fraction) {
5267 return Err(SqlError::Unsupported {
5268 feature: "sample fraction must be between 0 and 1".into(),
5269 });
5270 }
5271 let predicate = self
5272 .dataframe
5273 .parse_sql_expr(&format!("random() < {fraction}"))?;
5274 let df = self.dataframe.clone().filter(predicate)?;
5275 Ok(Box::new(self.with_new_dataframe(df, "sample")))
5276 }
5277
5278 async fn sort(
5279 &self,
5280 columns: &[&str],
5281 descending: &[bool],
5282 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5283 use datafusion::logical_expr::SortExpr;
5284 let exprs: Vec<SortExpr> = columns
5285 .iter()
5286 .zip(descending.iter())
5287 .map(|(col_name, desc)| datafusion::logical_expr::col(*col_name).sort(!desc, *desc))
5288 .collect();
5289 let df = self.dataframe.clone().sort(exprs)?;
5290 Ok(Box::new(self.with_new_dataframe(df, "sort")))
5291 }
5292
5293 async fn alias(&self, alias: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5294 let df = self.dataframe.clone().alias(alias)?;
5295 Ok(Box::new(self.with_new_dataframe(df, "alias")))
5296 }
5297
5298 async fn drop_columns(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5299 let df = self.dataframe.clone().drop_columns(columns)?;
5300 Ok(Box::new(self.with_new_dataframe(df, "drop")))
5301 }
5302
5303 async fn rename_column(&self, old: &str, new: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5304 let df = self.dataframe.clone().with_column_renamed(old, new)?;
5305 Ok(Box::new(self.with_new_dataframe(df, "rename")))
5306 }
5307
5308 async fn with_column(&self, name: &str, expr: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5309 let parsed = self.dataframe.parse_sql_expr(expr)?;
5310 let df = self.dataframe.clone().with_column(name, parsed)?;
5311 Ok(Box::new(self.with_new_dataframe(df, "with_column")))
5312 }
5313
5314 fn as_any(&self) -> &dyn std::any::Any {
5315 self
5316 }
5317
5318 async fn describe(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5319 let df = self.dataframe.clone().describe().await?;
5320 Ok(Box::new(self.with_new_dataframe(df, "describe")))
5321 }
5322
5323 async fn fill_null(
5324 &self,
5325 column: &str,
5326 value: &str,
5327 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5328 let expr = format!("COALESCE({column}, {value})");
5329 let parsed = self.dataframe.parse_sql_expr(&expr)?;
5330 let df = self.dataframe.clone().with_column(column, parsed)?;
5331 Ok(Box::new(self.with_new_dataframe(df, "fill_null")))
5332 }
5333
5334 async fn join(
5335 &self,
5336 right: &dyn KrishivDataFrameOps,
5337 how: &str,
5338 left_on: &[&str],
5339 right_on: &[&str],
5340 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5341 let right_sql = right
5342 .as_any()
5343 .downcast_ref::<SqlDataFrame>()
5344 .ok_or_else(|| SqlError::DataFusion {
5345 message: "right DataFrame must be SqlDataFrame for join".into(),
5346 })?;
5347 use datafusion::common::JoinType;
5348 let join_type = match how.to_lowercase().as_str() {
5349 "inner" => JoinType::Inner,
5350 "left" => JoinType::Left,
5351 "right" => JoinType::Right,
5352 "full" | "outer" => JoinType::Full,
5353 "leftsemi" | "left_semi" => JoinType::LeftSemi,
5354 "rightsemi" | "right_semi" => JoinType::RightSemi,
5355 "leftanti" | "left_anti" => JoinType::LeftAnti,
5356 "rightanti" | "right_anti" => JoinType::RightAnti,
5357 _ => {
5358 return Err(SqlError::DataFusion {
5359 message: format!("unsupported join type: {how}"),
5360 });
5361 }
5362 };
5363 let df = self.dataframe.clone().join(
5364 right_sql.dataframe.clone(),
5365 join_type,
5366 left_on,
5367 right_on,
5368 None,
5369 )?;
5370 Ok(Box::new(self.with_new_dataframe(df, "join")))
5371 }
5372
5373 async fn union(
5374 &self,
5375 right: &dyn KrishivDataFrameOps,
5376 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5377 let right_sql = right
5378 .as_any()
5379 .downcast_ref::<SqlDataFrame>()
5380 .ok_or_else(|| SqlError::DataFusion {
5381 message: "right DataFrame must be SqlDataFrame for union".into(),
5382 })?;
5383 let df = self.dataframe.clone().union(right_sql.dataframe.clone())?;
5384 Ok(Box::new(self.with_new_dataframe(df, "union")))
5385 }
5386
5387 async fn union_distinct(
5388 &self,
5389 right: &dyn KrishivDataFrameOps,
5390 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5391 let right = sql_dataframe(right, "union_distinct")?;
5392 let df = self
5393 .dataframe
5394 .clone()
5395 .union_distinct(right.dataframe.clone())?;
5396 Ok(Box::new(self.with_new_dataframe(df, "union_distinct")))
5397 }
5398
5399 async fn intersect(
5400 &self,
5401 right: &dyn KrishivDataFrameOps,
5402 distinct: bool,
5403 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5404 let right = sql_dataframe(right, "intersect")?;
5405 let df = if distinct {
5406 self.dataframe
5407 .clone()
5408 .intersect_distinct(right.dataframe.clone())?
5409 } else {
5410 self.dataframe.clone().intersect(right.dataframe.clone())?
5411 };
5412 Ok(Box::new(self.with_new_dataframe(df, "intersect")))
5413 }
5414
5415 async fn except(
5416 &self,
5417 right: &dyn KrishivDataFrameOps,
5418 distinct: bool,
5419 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5420 let right = sql_dataframe(right, "except")?;
5421 let df = if distinct {
5422 self.dataframe
5423 .clone()
5424 .except_distinct(right.dataframe.clone())?
5425 } else {
5426 self.dataframe.clone().except(right.dataframe.clone())?
5427 };
5428 Ok(Box::new(self.with_new_dataframe(df, "except")))
5429 }
5430
5431 async fn register_batches(&self, name: &str, batches: Vec<RecordBatch>) -> SqlResult<()> {
5432 let schema = batches
5433 .first()
5434 .map(|b| b.schema())
5435 .unwrap_or_else(|| Arc::new(arrow::datatypes::Schema::empty()));
5436 let mem_table =
5437 datafusion::datasource::MemTable::try_new(schema, vec![batches]).map_err(|e| {
5438 SqlError::DataFusion {
5439 message: e.to_string(),
5440 }
5441 })?;
5442 self.context
5443 .register_table(name, Arc::new(mem_table))
5444 .map_err(SqlError::from)?;
5445 Ok(())
5446 }
5447
5448 async fn deregister_table(&self, name: &str) -> SqlResult<()> {
5449 let _ = self
5450 .context
5451 .deregister_table(name)
5452 .map_err(SqlError::from)?;
5453 Ok(())
5454 }
5455
5456 async fn create_view(&self, name: &str, replace: bool) -> SqlResult<()> {
5457 let query = self
5458 .query_text
5459 .as_deref()
5460 .ok_or_else(|| SqlError::DataFusion {
5461 message: "create_view requires an SQL query string on the DataFrame".into(),
5462 })?;
5463 let or_replace = if replace { "OR REPLACE " } else { "" };
5464 let safe_name = quote_identifier(name);
5465 let view_sql = format!("CREATE {or_replace}VIEW {safe_name} AS {query}");
5466 self.context.sql(&view_sql).await?;
5467 Ok(())
5468 }
5469}
5470
5471use krishiv_common::sql_util::quote_identifier;
5472
5473#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5480fn call_args_from_str(s: &str) -> Vec<String> {
5481 let mut args: Vec<String> = Vec::new();
5482 let mut cur = String::new();
5483 let mut in_str = false;
5484 let mut after_str = false;
5485 for ch in s.chars() {
5486 if after_str {
5487 if ch == ',' {
5488 after_str = false;
5489 }
5490 continue;
5491 }
5492 if in_str {
5493 if ch == '\'' {
5494 in_str = false;
5495 after_str = true;
5496 args.push(std::mem::take(&mut cur));
5497 } else {
5498 cur.push(ch);
5499 }
5500 } else if ch == '\'' {
5501 in_str = true;
5502 } else if ch == ',' {
5503 let t = cur.trim().to_string();
5504 if !t.is_empty() {
5505 args.push(t);
5506 }
5507 cur.clear();
5508 } else {
5509 cur.push(ch);
5510 }
5511 }
5512 let t = cur.trim().to_string();
5513 if !t.is_empty() {
5514 args.push(t);
5515 }
5516 args
5517}
5518
5519#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5526fn iceberg_table_ident(table_ref: &str) -> SqlResult<iceberg::TableIdent> {
5527 let parts: Vec<&str> = table_ref.splitn(3, '.').collect();
5528 match parts.len() {
5529 2 => {
5530 let ns = iceberg::NamespaceIdent::from_vec(vec![
5531 parts.first().copied().unwrap_or("").to_string(),
5532 ])
5533 .map_err(|e| SqlError::DataFusion {
5534 message: e.to_string(),
5535 })?;
5536 Ok(iceberg::TableIdent::new(
5537 ns,
5538 parts.get(1).copied().unwrap_or("").to_string(),
5539 ))
5540 }
5541 3 => {
5542 let ns = iceberg::NamespaceIdent::from_vec(vec![
5543 parts.get(1).copied().unwrap_or("").to_string(),
5544 ])
5545 .map_err(|e| SqlError::DataFusion {
5546 message: e.to_string(),
5547 })?;
5548 Ok(iceberg::TableIdent::new(
5549 ns,
5550 parts.get(2).copied().unwrap_or("").to_string(),
5551 ))
5552 }
5553 _ => Err(SqlError::DataFusion {
5554 message: format!(
5555 "invalid table reference '{table_ref}': expected 'ns.table' or 'cat.ns.table'"
5556 ),
5557 }),
5558 }
5559}
5560
5561#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5566fn parse_call_duration(s: &str) -> SqlResult<chrono::Duration> {
5567 let s = s.trim();
5568 let mut it = s.splitn(2, ' ');
5569 let n: i64 = it
5570 .next()
5571 .and_then(|v| v.parse().ok())
5572 .ok_or_else(|| SqlError::DataFusion {
5573 message: format!("invalid duration value in '{s}'"),
5574 })?;
5575 let unit = it.next().unwrap_or("").trim().to_ascii_lowercase();
5576 match unit.trim_end_matches('s') {
5577 "day" => Ok(chrono::Duration::days(n)),
5578 "hour" => Ok(chrono::Duration::hours(n)),
5579 "week" => Ok(chrono::Duration::weeks(n)),
5580 "minute" | "min" => Ok(chrono::Duration::minutes(n)),
5581 _ => Err(SqlError::DataFusion {
5582 message: format!("unknown duration unit '{unit}' in '{s}'"),
5583 }),
5584 }
5585}
5586
5587#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5595fn parse_dml_delete(stmt: &str) -> Option<(String, String)> {
5596 use datafusion::sql::sqlparser::ast::{FromTable, Statement, TableFactor};
5597 use datafusion::sql::sqlparser::dialect::GenericDialect;
5598 use datafusion::sql::sqlparser::parser::Parser;
5599
5600 let mut stmts = Parser::parse_sql(&GenericDialect {}, stmt).ok()?;
5601 if stmts.len() != 1 {
5602 return None;
5603 }
5604 let Statement::Delete(delete) = stmts.remove(0) else {
5605 return None;
5606 };
5607 let tables = match delete.from {
5610 FromTable::WithFromKeyword(tables) | FromTable::WithoutKeyword(tables) => tables,
5611 };
5612 let first_from = tables.into_iter().next()?;
5613 let table_name = match first_from.relation {
5614 TableFactor::Table { name, .. } => name.to_string(),
5615 _ => return None,
5616 };
5617 let predicate = delete
5618 .selection
5619 .map(|e| e.to_string())
5620 .unwrap_or_else(|| "TRUE".to_string());
5621 Some((table_name, predicate))
5622}
5623
5624#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5626struct ParsedInsert {
5627 table_ref: String,
5629 columns: Vec<String>,
5635 inner_query: String,
5638}
5639
5640#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5646fn parse_dml_insert(stmt: &str) -> Option<ParsedInsert> {
5647 use datafusion::sql::sqlparser::ast::{Statement, TableObject};
5648 use datafusion::sql::sqlparser::dialect::GenericDialect;
5649 use datafusion::sql::sqlparser::parser::Parser;
5650
5651 let mut stmts = Parser::parse_sql(&GenericDialect {}, stmt).ok()?;
5652 if stmts.len() != 1 {
5653 return None;
5654 }
5655 let Statement::Insert(insert) = stmts.remove(0) else {
5656 return None;
5657 };
5658 let TableObject::TableName(name) = insert.table else {
5659 return None;
5660 };
5661 let inner_query = insert.source?.to_string();
5662 Some(ParsedInsert {
5663 table_ref: name.to_string(),
5664 columns: insert.columns.iter().map(|c| c.to_string()).collect(),
5665 inner_query,
5666 })
5667}
5668
5669#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5671struct ParsedCtas {
5672 table_ref: String,
5674 or_replace: bool,
5675 inner_query: String,
5677 partition_by: Vec<String>,
5680}
5681
5682#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5692fn extract_partitioned_by(stmt: &str) -> Option<(String, Vec<String>)> {
5693 let bytes = stmt.as_bytes();
5694 let upper = stmt.to_ascii_uppercase();
5695 let upper_bytes = upper.as_bytes();
5696 const NEEDLE: &[u8] = b"PARTITIONED";
5697
5698 fn is_ident_byte(b: u8) -> bool {
5699 b.is_ascii_alphanumeric() || b == b'_'
5700 }
5701 fn skip_quoted(bytes: &[u8], mut i: usize, quote: u8) -> usize {
5704 i += 1;
5705 while let Some(&b) = bytes.get(i) {
5706 if b == quote {
5707 if bytes.get(i + 1) == Some("e) {
5708 i += 2;
5709 continue;
5710 }
5711 return i + 1;
5712 }
5713 i += 1;
5714 }
5715 i
5716 }
5717
5718 let mut i = 0;
5719 while let Some(&b) = bytes.get(i) {
5720 match b {
5721 b'\'' | b'"' => i = skip_quoted(bytes, i, b),
5722 _ => {
5723 let at_needle = upper_bytes
5724 .get(i..)
5725 .is_some_and(|rest| rest.starts_with(NEEDLE))
5726 && (i == 0
5727 || !i
5728 .checked_sub(1)
5729 .and_then(|p| upper_bytes.get(p))
5730 .copied()
5731 .is_some_and(is_ident_byte));
5732 if at_needle {
5733 let mut j = i + NEEDLE.len();
5734 while bytes.get(j).is_some_and(u8::is_ascii_whitespace) {
5735 j += 1;
5736 }
5737 if j > i + NEEDLE.len()
5740 && upper_bytes
5741 .get(j..)
5742 .is_some_and(|rest| rest.starts_with(b"BY"))
5743 && !upper_bytes.get(j + 2).copied().is_some_and(is_ident_byte)
5744 {
5745 let mut k = j + 2;
5746 while bytes.get(k).is_some_and(u8::is_ascii_whitespace) {
5747 k += 1;
5748 }
5749 if bytes.get(k) == Some(&b'(') {
5750 let mut depth = 0i32;
5752 let mut c = k;
5753 let close = loop {
5754 match bytes.get(c) {
5755 None => return None,
5757 Some(b'(') => depth += 1,
5758 Some(b')') => {
5759 depth -= 1;
5760 if depth == 0 {
5761 break c;
5762 }
5763 }
5764 Some(&(q @ b'\'' | q @ b'"')) => {
5765 c = skip_quoted(bytes, c, q);
5766 continue;
5767 }
5768 Some(_) => {}
5769 }
5770 c += 1;
5771 };
5772 let body = stmt.get(k + 1..close)?;
5773 let head = stmt.get(..i)?.trim_end();
5774 let tail = stmt.get(close + 1..)?.trim_start();
5775 let items = split_top_level_commas(body);
5776 let mut remainder = String::with_capacity(stmt.len());
5777 remainder.push_str(head);
5778 remainder.push(' ');
5779 remainder.push_str(tail);
5780 return Some((remainder, items));
5781 }
5782 }
5783 }
5784 i += 1;
5785 }
5786 }
5787 }
5788 None
5789}
5790
5791#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5794fn split_top_level_commas(s: &str) -> Vec<String> {
5795 let bytes = s.as_bytes();
5796 let mut items = Vec::new();
5797 let mut depth = 0i32;
5798 let mut start = 0usize;
5799 let mut i = 0;
5800 while let Some(&b) = bytes.get(i) {
5801 match b {
5802 b'(' => depth += 1,
5803 b')' => depth -= 1,
5804 b'\'' | b'"' => {
5805 i += 1;
5806 while bytes.get(i).is_some_and(|&c| c != b) {
5807 i += 1;
5808 }
5809 }
5810 b',' if depth == 0 => {
5811 if let Some(item) = s.get(start..i).map(str::trim)
5812 && !item.is_empty()
5813 {
5814 items.push(item.to_string());
5815 }
5816 start = i + 1;
5817 }
5818 _ => {}
5819 }
5820 i += 1;
5821 }
5822 if let Some(last) = s.get(start..).map(str::trim)
5823 && !last.is_empty()
5824 {
5825 items.push(last.to_string());
5826 }
5827 items
5828}
5829
5830fn split_sql_statements(sql: &str) -> Vec<String> {
5838 let mut items = Vec::new();
5839 let mut start = 0usize;
5840 let mut chars = sql.char_indices().peekable();
5841 while let Some((i, c)) = chars.next() {
5842 match c {
5843 '\'' => {
5844 while let Some((_, c2)) = chars.next() {
5846 if c2 == '\'' {
5847 if chars.peek().is_some_and(|&(_, c3)| c3 == '\'') {
5848 chars.next();
5849 continue;
5850 }
5851 break;
5852 }
5853 }
5854 }
5855 '"' => {
5856 for (_, c2) in chars.by_ref() {
5857 if c2 == '"' {
5858 break;
5859 }
5860 }
5861 }
5862 '-' if chars.peek().is_some_and(|&(_, c2)| c2 == '-') => {
5863 for (_, c2) in chars.by_ref() {
5864 if c2 == '\n' {
5865 break;
5866 }
5867 }
5868 }
5869 '/' if chars.peek().is_some_and(|&(_, c2)| c2 == '*') => {
5870 chars.next();
5871 let mut star = false;
5872 for (_, c2) in chars.by_ref() {
5873 if star && c2 == '/' {
5874 break;
5875 }
5876 star = c2 == '*';
5877 }
5878 }
5879 ';' => {
5880 if let Some(piece) = sql.get(start..i).map(str::trim)
5881 && !piece.is_empty()
5882 {
5883 items.push(piece.to_string());
5884 }
5885 start = i + 1;
5887 }
5888 _ => {}
5889 }
5890 }
5891 if let Some(last) = sql.get(start..).map(str::trim)
5892 && !last.is_empty()
5893 {
5894 items.push(last.to_string());
5895 }
5896 items
5897}
5898
5899#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5907fn parse_ctas(stmt: &str) -> Option<ParsedCtas> {
5908 use datafusion::sql::sqlparser::ast::Statement;
5909 use datafusion::sql::sqlparser::dialect::GenericDialect;
5910 use datafusion::sql::sqlparser::parser::Parser;
5911
5912 let (stripped, partition_by) = match extract_partitioned_by(stmt) {
5913 Some((remainder, items)) => (remainder, items),
5914 None => (stmt.to_string(), Vec::new()),
5915 };
5916 let mut stmts = Parser::parse_sql(&GenericDialect {}, &stripped).ok()?;
5917 if stmts.len() != 1 {
5918 return None;
5919 }
5920 let Statement::CreateTable(create) = stmts.remove(0) else {
5921 return None;
5922 };
5923 if create.external || create.temporary {
5924 return None;
5925 }
5926 let inner_query = create.query?.to_string();
5927 Some(ParsedCtas {
5928 table_ref: create.name.to_string(),
5929 or_replace: create.or_replace,
5930 inner_query,
5931 partition_by,
5932 })
5933}
5934
5935#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5937struct ParsedUpdate {
5938 table_ref: String,
5939 assignments: Vec<(String, String)>,
5941 predicate: Option<String>,
5942}
5943
5944#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5950fn parse_dml_update(stmt: &str) -> Option<ParsedUpdate> {
5951 use datafusion::sql::sqlparser::ast::{Statement, TableFactor};
5952 use datafusion::sql::sqlparser::dialect::GenericDialect;
5953 use datafusion::sql::sqlparser::parser::Parser;
5954
5955 let mut stmts = Parser::parse_sql(&GenericDialect {}, stmt).ok()?;
5956 if stmts.len() != 1 {
5957 return None;
5958 }
5959 let Statement::Update(update) = stmts.remove(0) else {
5961 return None;
5962 };
5963 let table_name = match update.table.relation {
5964 TableFactor::Table { name, .. } => name.to_string(),
5965 _ => return None,
5966 };
5967 let parsed_assignments: Vec<(String, String)> = update
5969 .assignments
5970 .into_iter()
5971 .map(|a| {
5972 let col = a.target.to_string();
5974 let val = a.value.to_string();
5975 (col, val)
5976 })
5977 .collect();
5978 if parsed_assignments.is_empty() {
5979 return None;
5980 }
5981 Some(ParsedUpdate {
5982 table_ref: table_name,
5983 assignments: parsed_assignments,
5984 predicate: update.selection.map(|e| e.to_string()),
5985 })
5986}
5987
5988pub fn plan_sql(query: impl Into<String>) -> SqlResult<SqlPlan> {
5990 let query = query.into();
5991 if query.trim().is_empty() {
5992 return Err(SqlError::EmptyQuery);
5993 }
5994
5995 if let Some(stmt) = cep_sql::parse_match_recognize(&query)? {
5996 let logical_plan = cep_sql::plan_match_recognize(stmt, &query);
5997 let optimized = Optimizer::default().optimize(logical_plan)?;
5998 return Ok(SqlPlan {
5999 query,
6000 logical_plan: optimized.plan,
6001 });
6002 }
6003
6004 let logical_plan =
6005 LogicalPlan::new("sql-query", ExecutionKind::Batch).with_node(PlanNode::new(
6006 "sql",
6007 format!("sql: {}", query.trim()),
6008 ExecutionKind::Batch,
6009 ));
6010
6011 let optimized = Optimizer::default().optimize(logical_plan)?;
6012 Ok(SqlPlan {
6013 query,
6014 logical_plan: optimized.plan,
6015 })
6016}
6017
6018pub fn explain_sql(query: impl Into<String>) -> SqlResult<String> {
6020 let plan = plan_sql(query)?;
6021 Ok(plan.logical_plan().describe())
6022}
6023
6024pub fn explain_sql_optimized(query: impl Into<String>, optimizer: &Optimizer) -> SqlResult<String> {
6029 let plan = plan_sql(query)?;
6030 let result = optimizer.optimize(plan.logical_plan().clone())?;
6031 let mut output = result.plan.describe();
6032 let optimizer_line = result.describe();
6033 output.push('\n');
6034 output.push_str(&optimizer_line);
6035 Ok(output)
6036}
6037
6038pub fn explain_sql_with_cost(
6040 query: impl Into<String>,
6041 cost_model: &dyn CostModel,
6042) -> SqlResult<String> {
6043 let plan = plan_sql(query)?;
6044 let cost = cost_model.estimate(plan.logical_plan());
6045 let mut output = plan.logical_plan().describe();
6046 output.push_str(&format!(
6047 "\ncost: cpu_nanos={}, memory_bytes={}, network_bytes={}",
6048 cost.cpu_nanos, cost.memory_bytes, cost.network_bytes
6049 ));
6050 Ok(output)
6051}
6052
6053pub fn referenced_table_names(query: impl AsRef<str>) -> SqlResult<Vec<String>> {
6059 let query = query.as_ref();
6060 if query.trim().is_empty() {
6061 return Err(SqlError::EmptyQuery);
6062 }
6063
6064 let statements =
6065 Parser::parse_sql(&GenericDialect {}, query).map_err(|e| SqlError::DataFusion {
6066 message: format!("SQL parse error: {e}"),
6067 })?;
6068 let mut names = BTreeSet::new();
6069 let _ = visit_relations(&statements, |relation| {
6070 names.insert(relation.to_string());
6071 ControlFlow::<()>::Continue(())
6072 });
6073 Ok(names.into_iter().collect())
6074}
6075
6076pub fn pretty_batches(batches: &[RecordBatch]) -> SqlResult<String> {
6078 Ok(pretty_format_batches(batches)
6079 .map_err(|error| SqlError::DataFusion {
6080 message: error.to_string(),
6081 })?
6082 .to_string())
6083}
6084
6085#[cfg(test)]
6086mod sql_tests;