1#![forbid(unsafe_code)]
2
3use std::collections::{BTreeSet, HashMap, VecDeque};
9use std::fmt;
10use std::num::NonZeroUsize;
11use std::ops::ControlFlow;
12use std::path::Path;
13use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
14use std::sync::{Arc, Mutex, RwLock};
15
16use arrow::datatypes::SchemaRef;
17use arrow::record_batch::RecordBatch;
18use arrow::util::pretty::pretty_format_batches;
19use catalog::{InMemoryCatalog, datafusion_bridge::DataFusionCatalogBridge};
20use datafusion::dataframe::DataFrame as DataFusionDataFrame;
21use datafusion::prelude::{ParquetReadOptions, SessionContext};
22use datafusion::sql::sqlparser::{ast::visit_relations, dialect::GenericDialect, parser::Parser};
23use object_store::aws::AmazonS3Builder;
24
25use krishiv_plan::optimizer::{CostModel, Optimizer};
26use krishiv_plan::{ExecutionKind, LogicalPlan, PlanNode};
27
28fn python_udf_arrow_type(name: &str) -> arrow::datatypes::DataType {
40 use arrow::datatypes::DataType;
41 match name.trim().to_ascii_lowercase().as_str() {
42 "double" | "float64" | "float" => DataType::Float64,
43 "float32" | "real" => DataType::Float32,
44 "int" | "int64" | "bigint" | "long" => DataType::Int64,
45 "int32" | "integer" => DataType::Int32,
46 "bool" | "boolean" => DataType::Boolean,
47 "utf8" | "string" | "varchar" | "text" => DataType::Utf8,
48 _ => DataType::Utf8,
49 }
50}
51
52pub(crate) fn build_s3_object_store(
53 bucket: &str,
54) -> object_store::Result<std::sync::Arc<dyn object_store::ObjectStore>> {
55 let mut builder = AmazonS3Builder::from_env().with_bucket_name(bucket);
56 let mut has_endpoint = false;
57 if let Ok(endpoint) = std::env::var("AWS_ENDPOINT_URL")
58 && !endpoint.is_empty()
59 {
60 builder = builder.with_endpoint(endpoint).with_allow_http(true);
62 has_endpoint = true;
63 }
64 let has_key = std::env::var("AWS_ACCESS_KEY_ID")
65 .map(|k| !k.is_empty())
66 .unwrap_or(false);
67 if let Ok(key) = std::env::var("AWS_ACCESS_KEY_ID")
68 && !key.is_empty()
69 {
70 builder = builder.with_access_key_id(key);
71 }
72 if let Ok(secret) = std::env::var("AWS_SECRET_ACCESS_KEY")
73 && !secret.is_empty()
74 {
75 builder = builder.with_secret_access_key(secret);
76 }
77 if has_endpoint && !has_key {
84 builder = builder.with_skip_signature(true);
85 }
86 let region = std::env::var("AWS_REGION")
87 .or_else(|_| std::env::var("AWS_DEFAULT_REGION"))
88 .unwrap_or_else(|_| "us-east-1".to_string());
89 builder = builder.with_region(region);
90 Ok(std::sync::Arc::new(builder.build()?))
91}
92
93pub mod analyze;
94pub mod catalog;
95pub mod cep_sql;
96
97pub mod connector_table;
98pub mod coop_amplifiers;
99pub mod create_function_ddl;
100pub mod distributed_plan;
101pub mod grace_hash_join;
102pub mod grammar;
103pub mod incremental_view;
104pub mod introspection_sql;
105
106pub mod kafka_table;
107pub mod lakehouse;
108pub mod late_materialize;
109pub mod live_table;
110pub(crate) mod join_estimates;
113pub mod object_store_registry;
114pub mod pipeline_ddl;
115pub mod pivot_sql;
116pub mod python_udf;
117pub mod scalar_udf;
118pub mod semi_join_reduction;
119pub mod spark_sql_ext;
121pub mod runtime_filter_exec;
122pub mod spillable_join;
123pub mod sqlstate;
124pub mod subquery;
125pub mod unnest_sql;
126pub mod unspillable_headroom;
127
128pub mod coverage;
129mod higher_order_functions;
130mod json_functions;
131mod spark_functions;
132pub mod statement_completion;
133pub mod streaming;
134pub mod streaming_table_ddl;
135pub mod streaming_tvf;
136pub mod streaming_window_plan;
137mod udf;
138mod window_functions;
139
140pub use cep_sql::{
141 MatchRecognizeStatement, execute_streaming_match_recognize, parse_match_recognize,
142};
143pub use lakehouse::{AsOfTableRef, MergeResult, MergeTargetUnsupportedError, preprocess_as_of_sql};
144
145pub use grammar::{
146 FeatureEntry, FeatureStatus, feature_matrix, features_by_status, features_for_category,
147};
148pub use sqlstate::{SqlStateError, sqlstate_for};
149pub use streaming::{ContinuousInputError, ContinuousTableInput};
150
151pub type SqlResult<T> = Result<T, SqlError>;
153
154pub type SqlStream =
160 std::pin::Pin<Box<dyn futures::stream::Stream<Item = Result<RecordBatch, SqlError>> + Send>>;
161
162static EPHEMERAL_TABLE_COUNTER: AtomicU64 = AtomicU64::new(0);
165
166fn next_ephemeral_name(prefix: &str) -> String {
167 let id = EPHEMERAL_TABLE_COUNTER.fetch_add(1, Ordering::Relaxed);
168 format!("__{prefix}_{id}")
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176enum WindowFnRegistration {
177 Register,
179 Skip,
183}
184
185struct PlanCache {
191 map: HashMap<String, (datafusion::logical_expr::LogicalPlan, std::time::Instant)>,
192 order: VecDeque<String>,
193 max: usize,
194}
195
196const PLAN_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(30);
208
209impl PlanCache {
210 fn new(max: usize) -> Self {
211 Self {
212 map: HashMap::new(),
213 order: VecDeque::new(),
214 max,
215 }
216 }
217
218 fn get(&self, key: &str) -> Option<&datafusion::logical_expr::LogicalPlan> {
219 self.map
220 .get(key)
221 .filter(|(_, at)| at.elapsed() < PLAN_CACHE_TTL)
222 .map(|(plan, _)| plan)
223 }
224
225 fn insert(&mut self, key: String, plan: datafusion::logical_expr::LogicalPlan) {
226 if self.map.contains_key(&key) {
227 self.order.retain(|k| k != &key);
230 } else if self.map.len() >= self.max
231 && let Some(oldest) = self.order.pop_front()
232 {
233 self.map.remove(&oldest);
234 }
235 self.order.push_back(key.clone());
236 self.map.insert(key, (plan, std::time::Instant::now()));
237 }
238
239 fn clear(&mut self) {
240 self.map.clear();
241 self.order.clear();
242 }
243
244 #[cfg(test)]
245 fn is_empty(&self) -> bool {
246 self.map.is_empty()
247 }
248}
249
250#[derive(Debug, Clone, Default)]
252pub struct ParquetReaderOptions {
253 pub batch_size: Option<usize>,
255}
256
257#[derive(Debug, Clone, Default)]
259pub struct CsvReaderOptions {
260 pub delimiter: Option<char>,
262 pub has_header: Option<bool>,
264}
265
266#[derive(Debug, Clone, Default)]
268pub struct ParquetWriterOptions {
269 pub compression: Option<String>,
271 pub max_row_group_size: Option<usize>,
273}
274
275#[derive(Debug, Clone, Default)]
277pub struct CsvWriterOptions {
278 pub delimiter: Option<char>,
280 pub has_header: Option<bool>,
282}
283
284#[non_exhaustive]
286#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
287pub enum SqlError {
288 #[error("SQL query is empty")]
290 EmptyQuery,
291 #[error("table name is empty")]
293 EmptyTableName,
294 #[error("unsupported SQL feature: {feature}")]
296 Unsupported { feature: String },
297 #[error("invalid table function: {message}")]
299 InvalidTableFunction { message: String },
300 #[error("DataFusion error: {message}")]
302 DataFusion { message: String },
303 #[error(transparent)]
305 Optimizer(#[from] krishiv_plan::optimizer::OptimizerError),
306 #[error("access denied: {reason}")]
308 AccessDenied { reason: String },
309 #[error("operation {operation_id} was cancelled")]
311 OperationCancelled { operation_id: u64 },
312 #[error("query timed out after {timeout_ms} ms")]
314 Timeout { timeout_ms: u64 },
315}
316
317impl From<datafusion::error::DataFusionError> for SqlError {
318 fn from(value: datafusion::error::DataFusionError) -> Self {
319 Self::DataFusion {
320 message: value.to_string(),
321 }
322 }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct SqlPlan {
328 query: String,
329 logical_plan: LogicalPlan,
330}
331
332impl SqlPlan {
333 pub fn query(&self) -> &str {
335 &self.query
336 }
337
338 pub fn logical_plan(&self) -> &LogicalPlan {
340 &self.logical_plan
341 }
342}
343
344const PLAN_CACHE_MAX_ENTRIES: usize = 256;
356
357fn resolve_plan_cache_max_entries() -> usize {
358 std::env::var("KRISHIV_PLAN_CACHE_MAX_ENTRIES")
359 .ok()
360 .and_then(|v| v.parse().ok())
361 .filter(|&n| n > 0)
362 .unwrap_or(PLAN_CACHE_MAX_ENTRIES)
363}
364const STREAMING_CEP_MAX_ROWS_DEFAULT: usize = 100_000;
365
366pub fn resolve_streaming_match_recognize_limit(raw: Option<&str>) -> usize {
370 raw.and_then(|s| s.parse::<usize>().ok())
371 .filter(|n| *n > 0)
372 .unwrap_or(STREAMING_CEP_MAX_ROWS_DEFAULT)
373}
374
375pub fn streaming_match_recognize_limit_from_env() -> usize {
378 resolve_streaming_match_recognize_limit(
379 std::env::var("KRISHIV_MATCH_RECOGNIZE_STREAMING_LIMIT")
380 .ok()
381 .as_deref(),
382 )
383}
384
385pub fn resolve_query_memory_limit_bytes(raw: Option<&str>) -> Option<usize> {
389 raw.and_then(|s| s.trim().parse::<usize>().ok())
390 .filter(|n| *n > 0)
391}
392
393pub fn query_memory_limit_from_env() -> Option<usize> {
404 match std::env::var("KRISHIV_QUERY_MEMORY_LIMIT_BYTES").ok() {
405 Some(raw) => resolve_query_memory_limit_bytes(Some(&raw)),
408 None => cgroup_memory_limit_bytes()
409 .map(|limit| (limit / 4) as usize)
410 .filter(|&n| n > 0),
411 }
412}
413
414pub use krishiv_common::cgroup_memory_limit_bytes;
415
416pub use datafusion::execution::memory_pool::MemoryPool;
420
421pub use datafusion::execution::memory_pool::{MemoryConsumer, MemoryReservation};
428
429pub use datafusion::physical_plan::RecordBatchStream;
438
439pub fn process_query_pool() -> Option<&'static Arc<dyn MemoryPool>> {
450 static POOL: std::sync::LazyLock<Option<Arc<dyn MemoryPool>>> =
451 std::sync::LazyLock::new(|| {
452 let capacity = krishiv_common::ExecutorCapacity::detect();
453 let bytes = capacity.query_pool_bytes?;
454 tracing::info!(
455 capacity = %capacity.summary(),
456 "query memory: one shared FairSpillPool for this process"
457 );
458 Some(EngineMemory::shared_pool(
459 usize::try_from(bytes).unwrap_or(usize::MAX),
460 ))
461 });
462 POOL.as_ref()
463}
464
465fn guarded_fair_pool(bytes: usize) -> Arc<dyn MemoryPool> {
480 let inner: Arc<dyn MemoryPool> =
481 Arc::new(datafusion::execution::memory_pool::FairSpillPool::new(bytes));
482 Arc::new(crate::unspillable_headroom::UnspillableHeadroomPool::new(
483 inner,
484 bytes,
485 crate::unspillable_headroom::headroom_bytes(bytes),
486 ))
487}
488
489fn process_query_pool_fair_share_bytes() -> usize {
493 krishiv_common::ExecutorCapacity::detect()
494 .min_task_memory_share_bytes()
495 .map_or(usize::MAX, |bytes| {
496 usize::try_from(bytes).unwrap_or(usize::MAX)
497 })
498}
499
500#[derive(Clone)]
512pub enum EngineMemory {
513 Unbounded,
515 Private(usize),
518 Shared {
524 pool: Arc<dyn datafusion::execution::memory_pool::MemoryPool>,
526 fair_share_bytes: usize,
528 },
529}
530
531impl EngineMemory {
532 #[must_use]
535 pub fn from_limit(bytes: Option<usize>) -> Self {
536 bytes.map_or(Self::Unbounded, Self::Private)
537 }
538
539 #[must_use]
545 pub fn shared_pool(bytes: usize) -> Arc<dyn MemoryPool> {
546 guarded_fair_pool(bytes)
547 }
548
549 #[must_use]
561 pub fn for_this_process() -> Self {
562 match process_query_pool() {
563 Some(pool) => Self::Shared {
564 pool: Arc::clone(pool),
565 fair_share_bytes: process_query_pool_fair_share_bytes(),
566 },
567 None => Self::Unbounded,
568 }
569 }
570
571 #[must_use]
574 pub fn sizing_bytes(&self) -> Option<usize> {
575 match self {
576 Self::Unbounded => None,
577 Self::Private(bytes) => Some(*bytes),
578 Self::Shared {
579 fair_share_bytes, ..
580 } => Some(*fair_share_bytes),
581 }
582 }
583
584 fn pool(&self) -> Option<Arc<dyn datafusion::execution::memory_pool::MemoryPool>> {
586 match self {
587 Self::Unbounded => None,
588 Self::Private(bytes) => Some(guarded_fair_pool(*bytes)),
594 Self::Shared { pool, .. } => Some(Arc::clone(pool)),
595 }
596 }
597}
598
599impl fmt::Debug for EngineMemory {
600 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
601 match self {
602 Self::Unbounded => f.write_str("EngineMemory::Unbounded"),
603 Self::Private(bytes) => write!(f, "EngineMemory::Private({bytes})"),
604 Self::Shared {
605 fair_share_bytes, ..
606 } => write!(f, "EngineMemory::Shared(share={fair_share_bytes})"),
607 }
608 }
609}
610
611static RUNTIME_FILTERS_OVERRIDE: std::sync::atomic::AtomicU8 =
616 std::sync::atomic::AtomicU8::new(u8::MAX);
617
618#[doc(hidden)]
621pub fn set_runtime_filters_for_tests(enabled: bool) {
622 RUNTIME_FILTERS_OVERRIDE.store(u8::from(enabled), std::sync::atomic::Ordering::Relaxed);
623}
624
625pub fn runtime_filters_enabled_from_env() -> bool {
630 match RUNTIME_FILTERS_OVERRIDE.load(std::sync::atomic::Ordering::Relaxed) {
631 0 => return false,
632 1 => return true,
633 _ => {}
634 }
635 !matches!(
636 std::env::var("KRISHIV_RUNTIME_FILTERS")
637 .unwrap_or_default()
638 .trim()
639 .to_ascii_lowercase()
640 .as_str(),
641 "off" | "0" | "false" | "disabled"
642 )
643}
644
645pub fn batch_size_from_env() -> usize {
649 std::env::var("KRISHIV_BATCH_SIZE")
650 .ok()
651 .and_then(|v| v.parse::<usize>().ok())
652 .filter(|n| *n > 0)
653 .unwrap_or(8192)
654}
655
656pub fn default_parallelism_from_env() -> NonZeroUsize {
660 std::env::var("KRISHIV_TARGET_PARALLELISM")
661 .ok()
662 .and_then(|v| v.parse::<usize>().ok())
663 .and_then(NonZeroUsize::new)
664 .unwrap_or_else(|| std::thread::available_parallelism().unwrap_or(NonZeroUsize::MIN))
665}
666
667const DEFAULT_SORT_SPILL_RESERVATION_BYTES: usize = 10 * 1024 * 1024;
673
674const MIN_SORT_SPILL_RESERVATION_BYTES: usize = 64 * 1024;
677
678#[must_use]
720pub fn with_krishiv_optimizer_rules(
721 builder: datafusion::execution::session_state::SessionStateBuilder,
722) -> datafusion::execution::session_state::SessionStateBuilder {
723 with_krishiv_optimizer_rules_with_join_threshold(builder, None)
724}
725
726#[must_use]
729pub fn with_krishiv_optimizer_rules_with_join_threshold(
730 builder: datafusion::execution::session_state::SessionStateBuilder,
731 spill_join_build_bytes: Option<u64>,
732) -> datafusion::execution::session_state::SessionStateBuilder {
733 let spillable_join = match spill_join_build_bytes {
734 Some(bytes) => crate::spillable_join::SpillableJoinSelection::with_threshold(Some(bytes)),
747 None => crate::spillable_join::SpillableJoinSelection::from_capacity(),
748 }
749 .with_grace_where_plans_are_never_encoded();
757 builder
758 .with_physical_optimizer_rule(std::sync::Arc::new(
759 crate::coop_amplifiers::CooperativeAmplifiers::new(),
760 ))
761 .with_physical_optimizer_rule(std::sync::Arc::new(spillable_join))
767 .with_optimizer_rule(std::sync::Arc::new(
770 crate::semi_join_reduction::SemiJoinReductionThroughAggregate,
771 ))
772 .with_optimizer_rule(std::sync::Arc::new(
781 crate::semi_join_reduction::SemiJoinPushdownThroughInnerJoin::default(),
782 ))
783 .with_optimizer_rule(std::sync::Arc::new(
793 crate::semi_join_reduction::SemiJoinReductionFromSelectiveDimension::default(),
794 ))
795 .with_optimizer_rule(std::sync::Arc::new(
807 crate::late_materialize::LateMaterializeTopKAggregate::default(),
808 ))
809}
810
811pub(crate) fn build_single_node_session_config(
812 target_partitions: NonZeroUsize,
813 memory_limit_bytes: Option<usize>,
814) -> datafusion::prelude::SessionConfig {
815 let tp = target_partitions.get();
816 let batch_size = batch_size_from_env();
817 let mut config = datafusion::prelude::SessionConfig::new()
818 .with_target_partitions(tp)
819 .with_batch_size(batch_size)
820 .with_information_schema(true)
821 .set_bool(
822 "datafusion.optimizer.enable_round_robin_repartition",
823 tp > 1,
824 )
825 .set_bool(
830 "datafusion.optimizer.enable_dynamic_filter_pushdown",
831 runtime_filters_enabled_from_env(),
832 )
833 .set_bool(
834 "datafusion.optimizer.enable_join_dynamic_filter_pushdown",
835 runtime_filters_enabled_from_env(),
836 )
837 .set_bool(
838 "datafusion.optimizer.enable_topk_dynamic_filter_pushdown",
839 runtime_filters_enabled_from_env(),
840 )
841 .set_bool(
842 "datafusion.optimizer.enable_aggregate_dynamic_filter_pushdown",
843 runtime_filters_enabled_from_env(),
844 );
845 config.options_mut().sql_parser.dialect = datafusion::common::config::Dialect::DuckDB;
853 if let Some(limit) = memory_limit_bytes {
860 let scaled = (limit / 4).clamp(
861 MIN_SORT_SPILL_RESERVATION_BYTES,
862 DEFAULT_SORT_SPILL_RESERVATION_BYTES,
863 );
864 config = config.with_sort_spill_reservation_bytes(scaled);
865 }
866 config
867}
868
869#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
873type IcebergCatalogRegistry =
874 Arc<std::sync::RwLock<Vec<(Arc<catalog::unified::KrishivCatalog>, String)>>>;
875
876#[derive(Clone)]
877pub struct SqlEngine {
878 context: SessionContext,
879 target_parallelism: NonZeroUsize,
880 krishiv_catalog: Option<Arc<RwLock<InMemoryCatalog>>>,
881 udf_registry: Option<std::sync::Arc<std::sync::RwLock<krishiv_plan::udf::UdfRegistry>>>,
882 streaming_sources: Arc<RwLock<std::collections::HashSet<String>>>,
885 streaming_registration: Arc<Mutex<()>>,
887 has_streaming_sources: Arc<AtomicBool>,
892 udf_limits: Option<krishiv_plan::udf::ResourceLimits>,
895 udf_registry_version: Arc<AtomicU64>,
899 udf_last_synced_version: Arc<AtomicU64>,
902 plan_cache: Arc<Mutex<PlanCache>>,
908 shuffle_partitions: Arc<std::sync::RwLock<Option<u32>>>,
911 table_row_counts: Arc<std::sync::RwLock<HashMap<String, u64>>>,
916 memory_limit_bytes: Option<usize>,
921 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
925 iceberg_catalogs: IcebergCatalogRegistry,
926 live_table_registry: Arc<live_table::LiveTableRegistry>,
928 incremental_view_registry: Arc<incremental_view::IncrementalViewRegistry>,
930 pipeline_registry: Arc<pipeline_ddl::PipelineRegistry>,
932 operation_registry: Arc<OperationRegistry>,
934}
935
936impl fmt::Debug for SqlEngine {
937 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
938 f.debug_struct("SqlEngine")
939 .field("backend", &"datafusion")
940 .finish_non_exhaustive()
941 }
942}
943
944impl Default for SqlEngine {
945 fn default() -> Self {
946 Self::new()
947 }
948}
949
950impl SqlEngine {
951 pub fn new() -> Self {
965 Self::new_with_engine_memory(EngineMemory::for_this_process())
966 }
967
968 pub fn new_with_memory_limit(memory_limit_bytes: Option<usize>) -> Self {
980 Self::new_with_engine_memory(EngineMemory::from_limit(memory_limit_bytes))
981 }
982
983 pub fn new_with_engine_memory(engine_memory: EngineMemory) -> Self {
993 let parallelism = default_parallelism_from_env();
994 match Self::build_local(
995 None,
996 WindowFnRegistration::Register,
997 parallelism,
998 engine_memory.clone(),
999 ) {
1000 Ok(engine) => engine,
1001 Err(err) => {
1002 tracing::warn!(
1003 error = %err,
1004 "SqlEngine::new: window helper UDF registration failed; \
1005 window SQL functions will be unavailable, other queries are unaffected"
1006 );
1007 Self::build_local(
1008 None,
1009 WindowFnRegistration::Skip,
1010 parallelism,
1011 engine_memory.clone(),
1012 )
1013 .unwrap_or_else(|err| {
1014 tracing::error!(
1015 error = %err,
1016 "memory-limited DataFusion runtime construction failed; \
1017 falling back to an unbounded engine"
1018 );
1019 Self::build_local(
1020 None,
1021 WindowFnRegistration::Skip,
1022 parallelism,
1023 EngineMemory::Unbounded,
1024 )
1025 .unwrap_or_else(|_| Self::build_absolute_minimal(parallelism))
1026 })
1027 }
1028 }
1029 }
1030
1031 pub fn try_new() -> SqlResult<Self> {
1036 Self::build_local(
1037 None,
1038 WindowFnRegistration::Register,
1039 default_parallelism_from_env(),
1040 EngineMemory::for_this_process(),
1041 )
1042 }
1043
1044 pub fn with_in_memory_catalog(catalog: Arc<RwLock<InMemoryCatalog>>) -> SqlResult<Self> {
1046 if krishiv_common::profile_requires_fail_closed_metadata(
1047 krishiv_common::resolve_durability_profile(),
1048 ) {
1049 return Err(SqlError::DataFusion {
1050 message: String::from(
1051 "InMemoryCatalog is dev-only; configure a durable REST or file-backed \
1052 catalog for production deployments",
1053 ),
1054 });
1055 }
1056 Self::build_local(
1057 Some(catalog),
1058 WindowFnRegistration::Register,
1059 default_parallelism_from_env(),
1060 EngineMemory::for_this_process(),
1061 )
1062 }
1063
1064 #[must_use]
1075 pub fn with_target_parallelism(mut self, n: NonZeroUsize) -> Self {
1076 self.target_parallelism = n;
1077 self.apply_target_partitions(n);
1078 self
1079 }
1080
1081 fn apply_target_partitions(&self, n: NonZeroUsize) {
1090 let state_ref = self.context.state_ref();
1091 let mut state = state_ref.write();
1092 let options = state.config_mut().options_mut();
1093 options.execution.target_partitions = n.get();
1094 options.optimizer.enable_round_robin_repartition = n.get() > 1;
1095 }
1096
1097 pub fn target_parallelism(&self) -> NonZeroUsize {
1099 self.target_parallelism
1100 }
1101
1102 pub fn memory_limit_bytes(&self) -> Option<usize> {
1104 self.memory_limit_bytes
1105 }
1106
1107 pub fn session_context(&self) -> &SessionContext {
1113 &self.context
1114 }
1115
1116 pub fn shuffle_partitions(&self) -> Option<u32> {
1118 *self
1119 .shuffle_partitions
1120 .read()
1121 .unwrap_or_else(|e| e.into_inner())
1122 }
1123
1124 pub fn table_row_counts(&self) -> Arc<std::sync::RwLock<HashMap<String, u64>>> {
1130 Arc::clone(&self.table_row_counts)
1131 }
1132
1133 pub fn registered_table_names(&self) -> Vec<String> {
1139 let mut names = Vec::new();
1140 for catalog_name in self.context.catalog_names() {
1141 let Some(catalog) = self.context.catalog(&catalog_name) else {
1142 continue;
1143 };
1144 for schema_name in catalog.schema_names() {
1145 let Some(schema) = catalog.schema(&schema_name) else {
1146 continue;
1147 };
1148 names.extend(schema.table_names());
1149 }
1150 }
1151 names.sort();
1152 names.dedup();
1153 names
1154 }
1155
1156 fn make_sql_df(&self, name: &str, dataframe: DataFusionDataFrame) -> SqlDataFrame {
1159 SqlDataFrame::new(name, dataframe, self.table_row_counts())
1160 .with_context(self.context.clone())
1161 }
1162
1163 fn attach_query_metadata(&self, df: SqlDataFrame, query: &str) -> SqlDataFrame {
1165 let kind = if self.is_streaming_query(query).unwrap_or(false) {
1166 ExecutionKind::Streaming
1167 } else {
1168 ExecutionKind::Batch
1169 };
1170 df.with_query(query).with_execution_kind(kind)
1171 }
1172
1173 #[must_use]
1178 pub fn with_shuffle_partitions(self, n: Option<u32>) -> Self {
1179 if let Ok(mut guard) = self.shuffle_partitions.write() {
1180 *guard = n;
1181 }
1182 self
1183 }
1184
1185 fn build_local(
1195 krishiv_catalog: Option<Arc<RwLock<InMemoryCatalog>>>,
1196 window_fn_registration: WindowFnRegistration,
1197 target_partitions: NonZeroUsize,
1198 engine_memory: EngineMemory,
1199 ) -> SqlResult<Self> {
1200 let memory_limit_bytes = engine_memory.sizing_bytes();
1201 let streaming_sources: Arc<RwLock<std::collections::HashSet<String>>> =
1205 Arc::new(RwLock::new(std::collections::HashSet::new()));
1206
1207 let mut state_builder = with_krishiv_optimizer_rules(
1208 datafusion::execution::session_state::SessionStateBuilder::new()
1209 .with_default_features(),
1210 )
1211 .with_config(build_single_node_session_config(
1212 target_partitions,
1213 memory_limit_bytes,
1214 ));
1215 {
1216 let mut runtime_builder = datafusion::execution::runtime_env::RuntimeEnvBuilder::new()
1222 .with_object_store_registry(Arc::new(
1223 crate::object_store_registry::LazyCloudObjectStoreRegistry::new(),
1224 ));
1225 if let Some(pool) = engine_memory.pool() {
1226 runtime_builder = runtime_builder.with_memory_pool(pool);
1234 }
1235 let runtime_env = runtime_builder
1236 .build_arc()
1237 .map_err(|e| SqlError::DataFusion {
1238 message: format!(
1239 "failed to build DataFusion runtime \
1240 (memory limit {memory_limit_bytes:?} bytes): {e}"
1241 ),
1242 })?;
1243 state_builder = state_builder.with_runtime_env(runtime_env);
1244 }
1245 let mut state = state_builder.build();
1246 crate::connector_table::register_connector_table_factories(
1250 state.table_factories_mut(),
1251 streaming_sources.clone(),
1252 );
1253 let context = SessionContext::new_with_state(state);
1254 if let Some(catalog) = &krishiv_catalog {
1255 context.register_catalog(
1256 "krishiv",
1257 Arc::new(DataFusionCatalogBridge::new(catalog.clone())),
1258 );
1259 }
1260 if matches!(window_fn_registration, WindowFnRegistration::Register) {
1261 window_functions::register_window_functions(&context).map_err(|e| {
1262 SqlError::DataFusion {
1263 message: format!("failed to register window helper UDFs: {e}"),
1264 }
1265 })?;
1266 }
1267 json_functions::register_json_functions(&context).map_err(|e| SqlError::DataFusion {
1270 message: format!("failed to register JSON UDFs: {e}"),
1271 })?;
1272 higher_order_functions::register_higher_order_spark_functions(&context).map_err(|e| {
1275 SqlError::DataFusion {
1276 message: format!("failed to register higher-order UDFs: {e}"),
1277 }
1278 })?;
1279 spark_functions::register_spark_scalar_functions(&context).map_err(|e| {
1281 SqlError::DataFusion {
1282 message: format!("failed to register Spark scalar UDFs: {e}"),
1283 }
1284 })?;
1285 Ok(Self {
1286 context,
1287 target_parallelism: target_partitions,
1288 krishiv_catalog,
1289 udf_registry: None,
1290 streaming_sources,
1291 streaming_registration: Arc::new(Mutex::new(())),
1292 has_streaming_sources: Arc::new(AtomicBool::new(false)),
1293 udf_limits: None,
1294 udf_registry_version: Arc::new(AtomicU64::new(0)),
1295 udf_last_synced_version: Arc::new(AtomicU64::new(u64::MAX)),
1296 plan_cache: Arc::new(Mutex::new(PlanCache::new(resolve_plan_cache_max_entries()))),
1297 shuffle_partitions: Arc::new(std::sync::RwLock::new(None)),
1298 table_row_counts: Arc::new(std::sync::RwLock::new(HashMap::new())),
1299 memory_limit_bytes,
1300 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
1301 iceberg_catalogs: Arc::new(std::sync::RwLock::new(Vec::new())),
1302 live_table_registry: Arc::new(live_table::LiveTableRegistry::new()),
1303 incremental_view_registry: Arc::new(incremental_view::IncrementalViewRegistry::new()),
1304 pipeline_registry: Arc::new(pipeline_ddl::PipelineRegistry::new()),
1305 operation_registry: Arc::new(OperationRegistry::new()),
1306 })
1307 }
1308
1309 fn build_absolute_minimal(target_partitions: NonZeroUsize) -> Self {
1313 let streaming_sources: Arc<RwLock<std::collections::HashSet<String>>> =
1314 Arc::new(RwLock::new(std::collections::HashSet::new()));
1315 let mut state = with_krishiv_optimizer_rules(
1316 datafusion::execution::session_state::SessionStateBuilder::new()
1317 .with_default_features(),
1318 )
1319 .with_config(build_single_node_session_config(target_partitions, None))
1320 .build();
1321 crate::connector_table::register_connector_table_factories(
1322 state.table_factories_mut(),
1323 streaming_sources.clone(),
1324 );
1325 let context = SessionContext::new_with_state(state);
1326 Self {
1327 context,
1328 target_parallelism: target_partitions,
1329 krishiv_catalog: None,
1330 udf_registry: None,
1331 streaming_sources,
1332 streaming_registration: Arc::new(Mutex::new(())),
1333 has_streaming_sources: Arc::new(AtomicBool::new(false)),
1334 udf_limits: None,
1335 udf_registry_version: Arc::new(AtomicU64::new(0)),
1336 udf_last_synced_version: Arc::new(AtomicU64::new(u64::MAX)),
1337 plan_cache: Arc::new(Mutex::new(PlanCache::new(resolve_plan_cache_max_entries()))),
1338 shuffle_partitions: Arc::new(std::sync::RwLock::new(None)),
1339 table_row_counts: Arc::new(std::sync::RwLock::new(HashMap::new())),
1340 memory_limit_bytes: None,
1341 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
1342 iceberg_catalogs: Arc::new(std::sync::RwLock::new(Vec::new())),
1343 live_table_registry: Arc::new(live_table::LiveTableRegistry::new()),
1344 incremental_view_registry: Arc::new(incremental_view::IncrementalViewRegistry::new()),
1345 pipeline_registry: Arc::new(pipeline_ddl::PipelineRegistry::new()),
1346 operation_registry: Arc::new(OperationRegistry::new()),
1347 }
1348 }
1349
1350 pub fn register_streaming_table(
1361 &self,
1362 name: &str,
1363 schema: arrow::datatypes::SchemaRef,
1364 ) -> SqlResult<Arc<ContinuousTableInput>> {
1365 let _registration = self.lock_streaming_registration()?;
1366 self.validate_new_streaming_table(name, &schema)?;
1367 let (table, input) = crate::streaming::create_continuous_table(schema).map_err(|e| {
1368 SqlError::DataFusion {
1369 message: e.to_string(),
1370 }
1371 })?;
1372 self.register_new_streaming_provider(name, table)?;
1373 self.streaming_sources
1374 .write()
1375 .unwrap_or_else(|e| e.into_inner())
1376 .insert(name.to_string());
1377 self.has_streaming_sources.store(true, Ordering::Release);
1378 self.invalidate_plan_cache();
1379 Ok(input)
1380 }
1381
1382 pub fn register_streaming_table_with_capacity(
1387 &self,
1388 name: &str,
1389 schema: arrow::datatypes::SchemaRef,
1390 capacity: usize,
1391 ) -> SqlResult<Arc<ContinuousTableInput>> {
1392 let _registration = self.lock_streaming_registration()?;
1393 self.validate_new_streaming_table(name, &schema)?;
1394 let (table, input) = crate::streaming::create_continuous_table_with_capacity(
1395 schema, capacity,
1396 )
1397 .map_err(|e| SqlError::DataFusion {
1398 message: e.to_string(),
1399 })?;
1400 self.register_new_streaming_provider(name, table)?;
1401 self.streaming_sources
1402 .write()
1403 .unwrap_or_else(|e| e.into_inner())
1404 .insert(name.to_string());
1405 self.has_streaming_sources.store(true, Ordering::Release);
1406 self.invalidate_plan_cache();
1407 Ok(input)
1408 }
1409
1410 fn lock_streaming_registration(&self) -> SqlResult<std::sync::MutexGuard<'_, ()>> {
1411 self.streaming_registration
1412 .lock()
1413 .map_err(|error| SqlError::DataFusion {
1414 message: format!("streaming table registration lock poisoned: {error}"),
1415 })
1416 }
1417
1418 fn validate_new_streaming_table(
1419 &self,
1420 name: &str,
1421 schema: &arrow::datatypes::SchemaRef,
1422 ) -> SqlResult<()> {
1423 if name.trim().is_empty() {
1424 return Err(SqlError::EmptyTableName);
1425 }
1426 if schema.fields().is_empty() {
1427 return Err(SqlError::DataFusion {
1428 message: "streaming table schema must contain at least one field".into(),
1429 });
1430 }
1431 if self
1432 .context
1433 .table_exist(name)
1434 .map_err(|error| SqlError::DataFusion {
1435 message: error.to_string(),
1436 })?
1437 {
1438 return Err(SqlError::DataFusion {
1439 message: format!("table '{name}' is already registered"),
1440 });
1441 }
1442 Ok(())
1443 }
1444
1445 fn register_new_streaming_provider(
1446 &self,
1447 name: &str,
1448 table: Arc<dyn datafusion::catalog::TableProvider>,
1449 ) -> SqlResult<()> {
1450 let previous =
1451 self.context
1452 .register_table(name, table)
1453 .map_err(|error| SqlError::DataFusion {
1454 message: error.to_string(),
1455 })?;
1456 if let Some(previous) = previous {
1457 self.context
1458 .register_table(name, previous)
1459 .map_err(|error| SqlError::DataFusion {
1460 message: format!(
1461 "table '{name}' was concurrently registered and could not be restored: \
1462 {error}"
1463 ),
1464 })?;
1465 return Err(SqlError::DataFusion {
1466 message: format!("table '{name}' was concurrently registered"),
1467 });
1468 }
1469 Ok(())
1470 }
1471
1472 pub fn register_kafka_source(
1486 &self,
1487 table_name: impl AsRef<str>,
1488 schema: arrow::datatypes::SchemaRef,
1489 bootstrap_servers: impl Into<String>,
1490 topic: impl Into<String>,
1491 group_id: impl Into<String>,
1492 ) -> SqlResult<()> {
1493 let table_name = table_name.as_ref();
1494 if table_name.trim().is_empty() {
1495 return Err(SqlError::EmptyTableName);
1496 }
1497 let config = krishiv_connectors::kafka::KafkaConfig {
1498 bootstrap_servers: bootstrap_servers.into(),
1499 topic: topic.into(),
1500 group_id: group_id.into(),
1501 auto_commit_interval_ms: {
1502 let profile = krishiv_common::resolve_durability_profile();
1503 if krishiv_common::requires_manual_kafka_commit(profile) {
1504 None
1505 } else {
1506 Some(1_000)
1507 }
1508 },
1509 security_protocol: None,
1510 ssl_ca_location: None,
1511 ssl_certificate_location: None,
1512 ssl_key_location: None,
1513 ssl_key_password: None,
1514 sasl_username: None,
1515 sasl_password: None,
1516 sasl_mechanisms: None,
1517 enable_idempotence: None,
1518 transactional_id: None,
1519 };
1520 let table =
1521 crate::kafka_table::create_kafka_streaming_table(schema, config).map_err(|e| {
1522 SqlError::DataFusion {
1523 message: e.to_string(),
1524 }
1525 })?;
1526 if self
1527 .context
1528 .table_exist(table_name)
1529 .map_err(SqlError::from)?
1530 {
1531 let _ = self
1532 .context
1533 .deregister_table(table_name)
1534 .map_err(SqlError::from)?;
1535 }
1536 self.context
1537 .register_table(table_name, table)
1538 .map_err(|e| SqlError::DataFusion {
1539 message: e.to_string(),
1540 })?;
1541 self.streaming_sources
1542 .write()
1543 .unwrap_or_else(|e| e.into_inner())
1544 .insert(table_name.to_string());
1545 self.has_streaming_sources.store(true, Ordering::Release);
1546 self.invalidate_plan_cache();
1547 Ok(())
1548 }
1549
1550 pub async fn sql_to_kafka(
1560 &self,
1561 sql: impl AsRef<str>,
1562 bootstrap_servers: impl Into<String>,
1563 topic: impl Into<String>,
1564 ) -> SqlResult<u64> {
1565 use futures::StreamExt;
1566 use krishiv_connectors::Sink as _;
1567 use krishiv_connectors::kafka::{KafkaConfig, KafkaSink};
1568
1569 let config = KafkaConfig {
1570 bootstrap_servers: bootstrap_servers.into(),
1571 topic: topic.into(),
1572 group_id: "krishiv-sql-writer".into(),
1573 auto_commit_interval_ms: None,
1574 security_protocol: None,
1575 ssl_ca_location: None,
1576 ssl_certificate_location: None,
1577 ssl_key_location: None,
1578 ssl_key_password: None,
1579 sasl_username: None,
1580 sasl_password: None,
1581 sasl_mechanisms: None,
1582 enable_idempotence: None,
1583 transactional_id: None,
1584 };
1585 let mut sink = KafkaSink::new(config).map_err(|e| SqlError::DataFusion {
1586 message: e.to_string(),
1587 })?;
1588
1589 let df = self.sql(sql.as_ref()).await?;
1590 let mut stream = df.execute_stream().await?;
1591 let mut total_rows = 0u64;
1592
1593 while let Some(result) = stream.next().await {
1594 let batch = result.map_err(|e| SqlError::DataFusion {
1595 message: e.to_string(),
1596 })?;
1597 if batch.num_rows() > 0 {
1598 total_rows += batch.num_rows() as u64;
1599 sink.write_batch(batch)
1600 .await
1601 .map_err(|e| SqlError::DataFusion {
1602 message: e.to_string(),
1603 })?;
1604 }
1605 }
1606 sink.flush().await.map_err(|e| SqlError::DataFusion {
1607 message: e.to_string(),
1608 })?;
1609 Ok(total_rows)
1610 }
1611
1612 pub fn with_udf_limits(mut self, limits: krishiv_plan::udf::ResourceLimits) -> Self {
1616 self.udf_limits = Some(limits);
1617 self
1618 }
1619
1620 pub fn is_streaming_source(&self, table_name: &str) -> bool {
1622 self.streaming_sources
1623 .read()
1624 .unwrap_or_else(|e| e.into_inner())
1625 .contains(table_name)
1626 }
1627
1628 pub fn register_streaming_source_name(&self, table_name: impl Into<String>) -> SqlResult<()> {
1637 let name: String = table_name.into();
1638 if name.trim().is_empty() {
1639 return Err(SqlError::EmptyTableName);
1640 }
1641 self.streaming_sources
1642 .write()
1643 .unwrap_or_else(|e| e.into_inner())
1644 .insert(name);
1645 self.has_streaming_sources.store(true, Ordering::Release);
1646 self.invalidate_plan_cache();
1647 Ok(())
1648 }
1649
1650 pub fn deregister_streaming_source(&self, name: &str) -> SqlResult<()> {
1656 if name.trim().is_empty() {
1657 return Err(SqlError::EmptyTableName);
1658 }
1659 let _ = self
1661 .context
1662 .deregister_table(name)
1663 .map_err(SqlError::from)?;
1664 {
1665 let mut sources = self
1666 .streaming_sources
1667 .write()
1668 .unwrap_or_else(|e| e.into_inner());
1669 sources.remove(name);
1670 if sources.is_empty() {
1671 self.has_streaming_sources.store(false, Ordering::Release);
1672 }
1673 self.invalidate_plan_cache();
1677 }
1678 Ok(())
1679 }
1680
1681 pub fn live_table_registry(&self) -> &Arc<live_table::LiveTableRegistry> {
1683 &self.live_table_registry
1684 }
1685
1686 pub fn incremental_view_registry(&self) -> &Arc<incremental_view::IncrementalViewRegistry> {
1688 &self.incremental_view_registry
1689 }
1690
1691 pub fn pipeline_registry(&self) -> &Arc<pipeline_ddl::PipelineRegistry> {
1693 &self.pipeline_registry
1694 }
1695
1696 pub fn operation_registry(&self) -> &Arc<OperationRegistry> {
1698 &self.operation_registry
1699 }
1700
1701 pub fn deregister_table(&self, name: &str) -> SqlResult<()> {
1720 if name.trim().is_empty() {
1721 return Err(SqlError::EmptyTableName);
1722 }
1723 let _ = self
1724 .context
1725 .deregister_table(name)
1726 .map_err(SqlError::from)?;
1727 {
1728 let mut sources = self
1729 .streaming_sources
1730 .write()
1731 .unwrap_or_else(|e| e.into_inner());
1732 sources.remove(name);
1733 if sources.is_empty() {
1734 self.has_streaming_sources.store(false, Ordering::Release);
1735 }
1736 self.invalidate_plan_cache();
1741 }
1742 Ok(())
1743 }
1744
1745 pub fn register_table_udf_fn(
1769 &self,
1770 name: impl Into<String>,
1771 schema: arrow::datatypes::Schema,
1772 f: impl Fn(
1773 &[krishiv_plan::udf::ScalarValue],
1774 ) -> Result<arrow::record_batch::RecordBatch, krishiv_plan::udf::UdfError>
1775 + Send
1776 + Sync
1777 + 'static,
1778 ) -> SqlResult<()> {
1779 let udf =
1780 create_function_ddl::ClosureTableUdf::try_new(name, schema, std::sync::Arc::new(f))
1781 .map_err(|error| SqlError::InvalidTableFunction {
1782 message: error.to_string(),
1783 })?;
1784 if let Some(registry) = &self.udf_registry {
1785 let mut guard = registry.write().map_err(|e| SqlError::DataFusion {
1786 message: e.to_string(),
1787 })?;
1788 guard.register_table(std::sync::Arc::new(udf.clone()));
1789 }
1790 udf::register_single_table_udf(&self.context, std::sync::Arc::new(udf))
1791 .map_err(SqlError::from)
1792 }
1793
1794 pub fn is_streaming_query(&self, sql: &str) -> SqlResult<bool> {
1796 if !self.has_streaming_sources.load(Ordering::Acquire) {
1799 return Ok(false);
1800 }
1801 let sources = self
1802 .streaming_sources
1803 .read()
1804 .unwrap_or_else(|e| e.into_inner());
1805 if sources.is_empty() {
1806 return Ok(false);
1807 }
1808 let dialect = GenericDialect {};
1809 let statements = Parser::parse_sql(&dialect, sql).map_err(|e| SqlError::DataFusion {
1810 message: e.to_string(),
1811 })?;
1812 for stmt in &statements {
1813 let mut is_streaming = false;
1814 let _ = visit_relations(stmt, |relation| {
1815 let full = relation.to_string();
1818 let table_name = full.split('.').next_back().unwrap_or(&full);
1819 if sources.contains(table_name) {
1820 is_streaming = true;
1821 return ControlFlow::Break(());
1822 }
1823 ControlFlow::Continue(())
1824 });
1825 if is_streaming {
1826 return Ok(true);
1827 }
1828 }
1829 Ok(false)
1830 }
1831
1832 pub fn krishiv_catalog(&self) -> Option<&Arc<RwLock<InMemoryCatalog>>> {
1834 self.krishiv_catalog.as_ref()
1835 }
1836
1837 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
1846 #[must_use]
1847 pub fn with_iceberg_catalog(
1848 self,
1849 catalog: std::sync::Arc<catalog::unified::KrishivCatalog>,
1850 catalog_name: impl Into<String>,
1851 ) -> Self {
1852 self.register_iceberg_catalog(catalog, catalog_name);
1853 self
1854 }
1855
1856 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
1863 pub fn register_iceberg_catalog(
1864 &self,
1865 catalog: std::sync::Arc<catalog::unified::KrishivCatalog>,
1866 catalog_name: impl Into<String>,
1867 ) {
1868 let catalog_name = catalog_name.into();
1869 let bridge = catalog::iceberg_catalog_bridge::IcebergCatalogBridge::new(
1870 Arc::clone(&catalog),
1871 catalog_name.clone(),
1872 );
1873 self.context
1874 .register_catalog(catalog_name.clone(), Arc::new(bridge));
1875 self.iceberg_catalogs
1876 .write()
1877 .unwrap_or_else(|e| e.into_inner())
1878 .push((catalog, catalog_name));
1879 self.invalidate_plan_cache();
1880 }
1881
1882 pub async fn register_iceberg_rest_catalog_from_env(&self) -> Result<bool, String> {
1894 #[cfg(feature = "rest-catalog")]
1895 {
1896 let uri = match std::env::var("KRISHIV_ICEBERG_REST_URI") {
1897 Ok(uri) => uri,
1898 Err(_) => return Ok(false),
1899 };
1900 let warehouse = std::env::var("KRISHIV_ICEBERG_REST_WAREHOUSE").unwrap_or_default();
1901 let token = std::env::var("KRISHIV_ICEBERG_REST_TOKEN").ok();
1902 let name =
1907 std::env::var("KRISHIV_ICEBERG_REST_NAME").unwrap_or_else(|_| String::from("main"));
1908 self.register_s3_object_store_for_warehouse(&warehouse)?;
1914 let catalog = std::sync::Arc::new(
1915 catalog::unified::KrishivCatalog::rest(&uri, &warehouse, token.as_deref())
1916 .await
1917 .map_err(|e| format!("iceberg REST catalog at {uri}: {e}"))?,
1918 );
1919 self.register_iceberg_catalog(std::sync::Arc::clone(&catalog), &name);
1920 if name != "krishiv" {
1926 self.register_iceberg_catalog(catalog, "krishiv");
1927 }
1928 Ok(true)
1929 }
1930 #[cfg(not(feature = "rest-catalog"))]
1931 {
1932 Ok(false)
1933 }
1934 }
1935
1936 #[must_use]
1938 pub fn with_udf_registry(
1939 mut self,
1940 registry: std::sync::Arc<std::sync::RwLock<krishiv_plan::udf::UdfRegistry>>,
1941 ) -> Self {
1942 self.udf_registry = Some(registry);
1943 self.bump_udf_version();
1945 self
1946 }
1947
1948 pub(crate) fn bump_udf_version(&self) {
1951 self.udf_registry_version.fetch_add(1, Ordering::Release);
1952 }
1953
1954 fn invalidate_plan_cache(&self) {
1959 match self.plan_cache.lock() {
1960 Ok(mut cache) => cache.clear(),
1961 Err(poisoned) => poisoned.into_inner().clear(),
1962 }
1963 }
1964
1965 pub fn clear_plan_cache(&self) {
1968 self.invalidate_plan_cache();
1969 }
1970
1971 pub async fn register_python_udfs_from_sql(&self, sql: &str) -> SqlResult<String> {
1982 const SCALAR_PREFIX: &str = "/* krishiv-register-python-udf:";
1983 const AGG_PREFIX: &str = "/* krishiv-register-python-udaf:";
1984 if !sql.contains(SCALAR_PREFIX) && !sql.contains(AGG_PREFIX) {
1985 return Ok(sql.to_string());
1986 }
1987 let mut out = String::with_capacity(sql.len());
1988 let mut rest = sql;
1989 loop {
1990 let agg = rest.find(AGG_PREFIX).map(|i| (i, true, AGG_PREFIX.len()));
1993 let scalar = rest
1994 .find(SCALAR_PREFIX)
1995 .map(|i| (i, false, SCALAR_PREFIX.len()));
1996 let next = match (agg, scalar) {
1997 (Some(a), Some(s)) => Some(if a.0 <= s.0 { a } else { s }),
1998 (Some(a), None) => Some(a),
1999 (None, Some(s)) => Some(s),
2000 (None, None) => None,
2001 };
2002 let Some((start, is_aggregate, prefix_len)) = next else {
2003 break;
2004 };
2005 out.push_str(&rest[..start]);
2006 let after = &rest[start + prefix_len..];
2007 let Some(end) = after.find(" */") else {
2008 out.push_str(&rest[start..]);
2010 return Ok(out);
2011 };
2012 self.register_python_udf_directive(&after[..end], is_aggregate)
2013 .await?;
2014 rest = &after[end + " */".len()..];
2015 }
2016 out.push_str(rest);
2017 Ok(out)
2018 }
2019
2020 async fn register_python_udf_directive(&self, body: &str, is_aggregate: bool) -> SqlResult<()> {
2023 use base64::Engine as _;
2024 let mut parts = body.splitn(4, ':');
2025 let (name, in_types, out_type, pickle_b64) =
2026 match (parts.next(), parts.next(), parts.next(), parts.next()) {
2027 (Some(n), Some(i), Some(o), Some(p)) => (n, i, o, p),
2028 _ => {
2029 return Err(SqlError::DataFusion {
2030 message: "malformed python-udf directive".into(),
2031 });
2032 }
2033 };
2034 let input_types: Vec<String> = if in_types.is_empty() {
2035 Vec::new()
2036 } else {
2037 in_types.split(',').map(str::to_string).collect()
2038 };
2039 let pickle = base64::engine::general_purpose::STANDARD
2040 .decode(pickle_b64)
2041 .map_err(|e| SqlError::DataFusion {
2042 message: format!("invalid python-udf pickle base64: {e}"),
2043 })?;
2044 if is_aggregate {
2045 self.register_python_udaf(name, &pickle, &input_types, out_type)
2046 .await
2047 } else {
2048 self.register_python_udf(name, &pickle, &input_types, out_type)
2049 .await
2050 }
2051 }
2052
2053 pub async fn register_python_udf(
2059 &self,
2060 name: &str,
2061 pickle: &[u8],
2062 input_types: &[String],
2063 output_type: &str,
2064 ) -> SqlResult<()> {
2065 use arrow::datatypes::{Field, Schema};
2066 let Some(registry) = &self.udf_registry else {
2067 return Err(SqlError::DataFusion {
2068 message: "cannot register a python UDF: engine has no UDF registry".into(),
2069 });
2070 };
2071 let input_fields: Vec<Field> = input_types
2072 .iter()
2073 .enumerate()
2074 .map(|(i, t)| Field::new(format!("a{i}"), python_udf_arrow_type(t), true))
2075 .collect();
2076 let input_schema = Schema::new(input_fields);
2077 let output_field = Field::new("out", python_udf_arrow_type(output_type), true);
2078 let pool = crate::python_udf::global_pool().map_err(|e| SqlError::DataFusion {
2079 message: format!("python UDF worker unavailable: {e:?}"),
2080 })?;
2081 let udf = std::sync::Arc::new(crate::python_udf::PythonWorkerUdf::new(
2082 name,
2083 pickle.to_vec(),
2084 input_schema,
2085 output_field,
2086 pool,
2087 ));
2088 registry
2089 .write()
2090 .map_err(|e| SqlError::DataFusion {
2091 message: e.to_string(),
2092 })?
2093 .register_scalar(udf);
2094 self.udf_registry_version
2095 .fetch_add(1, std::sync::atomic::Ordering::Release);
2096 self.sync_scalar_udfs().await
2097 }
2098
2099 pub async fn register_python_udaf(
2106 &self,
2107 name: &str,
2108 pickle: &[u8],
2109 input_types: &[String],
2110 output_type: &str,
2111 ) -> SqlResult<()> {
2112 use arrow::datatypes::{Field, Schema};
2113 let Some(registry) = &self.udf_registry else {
2114 return Err(SqlError::DataFusion {
2115 message: "cannot register a python UDAF: engine has no UDF registry".into(),
2116 });
2117 };
2118 let input_fields: Vec<Field> = input_types
2119 .iter()
2120 .enumerate()
2121 .map(|(i, t)| Field::new(format!("a{i}"), python_udf_arrow_type(t), true))
2122 .collect();
2123 let input_schema = Schema::new(input_fields);
2124 let output_field = Field::new("out", python_udf_arrow_type(output_type), true);
2125 let pool = crate::python_udf::global_pool().map_err(|e| SqlError::DataFusion {
2126 message: format!("python UDF worker unavailable: {e:?}"),
2127 })?;
2128 let udf = std::sync::Arc::new(crate::python_udf::PythonWorkerAggregateUdf::new(
2129 name,
2130 pickle.to_vec(),
2131 input_schema,
2132 output_field,
2133 pool,
2134 ));
2135 registry
2136 .write()
2137 .map_err(|e| SqlError::DataFusion {
2138 message: e.to_string(),
2139 })?
2140 .register_aggregate(udf);
2141 self.udf_registry_version
2142 .fetch_add(1, std::sync::atomic::Ordering::Release);
2143 self.sync_aggregate_udfs().await
2144 }
2145
2146 pub async fn sync_scalar_udfs(&self) -> SqlResult<()> {
2147 let Some(registry) = &self.udf_registry else {
2148 return Ok(());
2149 };
2150 let guard = registry.read().map_err(|e| SqlError::DataFusion {
2151 message: e.to_string(),
2152 })?;
2153 let limits = self.udf_limits.clone().unwrap_or_default();
2154 udf::sync_scalar_udfs_with_limits(&self.context, &guard, limits).map_err(|e| {
2155 SqlError::DataFusion {
2156 message: e.to_string(),
2157 }
2158 })
2159 }
2160
2161 pub async fn sync_scalar_udfs_with_limits(
2166 &self,
2167 limits: krishiv_plan::udf::ResourceLimits,
2168 ) -> SqlResult<()> {
2169 self.sync_scalar_udfs_with_limits_for_profile(
2170 limits,
2171 krishiv_common::resolve_durability_profile(),
2172 )
2173 .await
2174 }
2175
2176 pub async fn sync_scalar_udfs_with_limits_for_profile(
2178 &self,
2179 limits: krishiv_plan::udf::ResourceLimits,
2180 profile: krishiv_common::DurabilityProfile,
2181 ) -> SqlResult<()> {
2182 self.sync_scalar_udfs_with_limits_for_policy(
2183 limits,
2184 krishiv_common::NativeScalarUdfPolicy::resolve(profile),
2185 )
2186 .await
2187 }
2188
2189 pub async fn sync_scalar_udfs_with_limits_for_policy(
2191 &self,
2192 limits: krishiv_plan::udf::ResourceLimits,
2193 policy: krishiv_common::NativeScalarUdfPolicy,
2194 ) -> SqlResult<()> {
2195 let Some(registry) = &self.udf_registry else {
2196 return Ok(());
2197 };
2198 let guard = registry.read().map_err(|e| SqlError::DataFusion {
2199 message: e.to_string(),
2200 })?;
2201 udf::sync_scalar_udfs_with_limits_for_policy(&self.context, &guard, limits, policy).map_err(
2202 |e| SqlError::DataFusion {
2203 message: e.to_string(),
2204 },
2205 )
2206 }
2207
2208 pub async fn sync_aggregate_udfs(&self) -> SqlResult<()> {
2210 let Some(registry) = &self.udf_registry else {
2211 return Ok(());
2212 };
2213 let guard = registry.read().map_err(|e| SqlError::DataFusion {
2214 message: e.to_string(),
2215 })?;
2216 udf::sync_aggregate_udfs(&self.context, &guard).map_err(|e| SqlError::DataFusion {
2217 message: e.to_string(),
2218 })
2219 }
2220
2221 pub async fn sync_table_udfs(&self) -> SqlResult<()> {
2223 let Some(registry) = &self.udf_registry else {
2224 return Ok(());
2225 };
2226 let guard = registry.read().map_err(|e| SqlError::DataFusion {
2227 message: e.to_string(),
2228 })?;
2229 udf::sync_table_udfs(&self.context, &guard).map_err(|e| SqlError::DataFusion {
2230 message: e.to_string(),
2231 })
2232 }
2233
2234 pub async fn sync_all_udfs(&self) -> SqlResult<()> {
2236 self.sync_scalar_udfs().await?;
2237 self.sync_aggregate_udfs().await?;
2238 self.sync_table_udfs().await?;
2239 Ok(())
2240 }
2241
2242 pub(crate) fn register_s3_object_store_for_warehouse(&self, path: &str) -> Result<(), String> {
2249 if !(path.starts_with("s3://") || path.starts_with("s3a://")) {
2250 return Ok(());
2251 }
2252 let url = url::Url::parse(path).map_err(|e| format!("invalid s3 url {path}: {e}"))?;
2253 let bucket = url.host_str().unwrap_or_default();
2254 let store_url = url::Url::parse(&format!("s3://{bucket}"))
2256 .map_err(|e| format!("invalid s3 bucket url: {e}"))?;
2257 let store = build_s3_object_store(bucket).map_err(|e| format!("s3 store init: {e}"))?;
2258 self.context.register_object_store(&store_url, store);
2259 Ok(())
2260 }
2261
2262 pub async fn register_parquet(
2264 &self,
2265 table_name: impl AsRef<str>,
2266 path: impl AsRef<Path>,
2267 ) -> SqlResult<()> {
2268 self.register_parquet_with_primary_key(table_name, path, &[] as &[String])
2269 .await
2270 }
2271
2272 pub async fn register_parquet_with_primary_key<S: AsRef<str>>(
2290 &self,
2291 table_name: impl AsRef<str>,
2292 path: impl AsRef<Path>,
2293 primary_key: &[S],
2294 ) -> SqlResult<()> {
2295 let table_name = table_name.as_ref();
2296 if table_name.trim().is_empty() {
2297 return Err(SqlError::EmptyTableName);
2298 }
2299
2300 let path = path.as_ref().to_string_lossy().into_owned();
2301
2302 self.register_s3_object_store_for_warehouse(&path)
2305 .map_err(|message| SqlError::DataFusion { message })?;
2306
2307 if self
2308 .context
2309 .table_exist(table_name)
2310 .map_err(SqlError::from)?
2311 {
2312 let _ = self
2313 .context
2314 .deregister_table(table_name)
2315 .map_err(SqlError::from)?;
2316 }
2317 let spec = crate::distributed_plan::ParquetTableSpec::new(table_name, path)
2324 .with_primary_key(primary_key.iter().map(|c| c.as_ref().to_owned()));
2325 crate::distributed_plan::register_parquet_table(&self.context, &spec).await?;
2326 if let Ok(provider) = self.context.table_provider(table_name).await
2328 && let Some(stats) = provider.statistics()
2329 && let Some(n) = stats.num_rows.get_value()
2330 && let Ok(mut counts) = self.table_row_counts.write()
2331 {
2332 counts.insert(table_name.to_string(), *n as u64);
2333 }
2334 self.invalidate_plan_cache();
2335 Ok(())
2336 }
2337
2338 pub async fn read_parquet(&self, path: impl AsRef<Path>) -> SqlResult<SqlDataFrame> {
2340 let path = path.as_ref().to_string_lossy().into_owned();
2341 let dataframe = self
2342 .context
2343 .read_parquet(path, ParquetReadOptions::default())
2344 .await?;
2345 Ok(self.make_sql_df("parquet-read", dataframe))
2346 }
2347
2348 pub async fn register_record_batches(
2354 &self,
2355 table_name: impl AsRef<str>,
2356 batches: Vec<RecordBatch>,
2357 ) -> SqlResult<()> {
2358 use std::sync::Arc;
2359 let table_name = table_name.as_ref();
2360 if table_name.trim().is_empty() {
2361 return Err(SqlError::EmptyTableName);
2362 }
2363 if batches.is_empty() {
2364 return Ok(());
2365 }
2366 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
2367 let schema = batches
2368 .first()
2369 .ok_or_else(|| SqlError::DataFusion {
2370 message: "empty batch list".into(),
2371 })?
2372 .schema();
2373 let mem_table =
2374 datafusion::datasource::MemTable::try_new(schema, vec![batches]).map_err(|e| {
2375 SqlError::DataFusion {
2376 message: e.to_string(),
2377 }
2378 })?;
2379 if self
2380 .context
2381 .table_exist(table_name)
2382 .map_err(SqlError::from)?
2383 {
2384 let _ = self
2385 .context
2386 .deregister_table(table_name)
2387 .map_err(SqlError::from)?;
2388 }
2389 self.context
2390 .register_table(table_name, Arc::new(mem_table))
2391 .map_err(|e| SqlError::DataFusion {
2392 message: e.to_string(),
2393 })?;
2394 if total_rows > 0
2395 && let Ok(mut counts) = self.table_row_counts.write()
2396 {
2397 counts.insert(table_name.to_string(), total_rows as u64);
2398 }
2399 self.invalidate_plan_cache();
2400 Ok(())
2401 }
2402
2403 pub async fn read_parquet_with_options(
2405 &self,
2406 path: impl AsRef<Path>,
2407 opts: &ParquetReaderOptions,
2408 ) -> SqlResult<SqlDataFrame> {
2409 let path = path.as_ref().to_string_lossy().into_owned();
2410 let mut options = datafusion::prelude::ParquetReadOptions::default();
2411 if opts.batch_size.is_some() {
2412 options = options.parquet_pruning(true);
2413 }
2414 let dataframe = self.context.read_parquet(path, options).await?;
2420 Ok(self.make_sql_df("parquet-read", dataframe))
2421 }
2422
2423 pub async fn read_csv(&self, path: impl AsRef<Path>) -> SqlResult<SqlDataFrame> {
2425 self.read_csv_with_options(path, &CsvReaderOptions::default())
2426 .await
2427 }
2428
2429 pub async fn read_csv_with_options(
2431 &self,
2432 path: impl AsRef<Path>,
2433 opts: &CsvReaderOptions,
2434 ) -> SqlResult<SqlDataFrame> {
2435 let path = path.as_ref().to_string_lossy().into_owned();
2436 let mut options = datafusion::prelude::CsvReadOptions::new();
2437 if let Some(delim) = opts.delimiter {
2438 options = options.delimiter(delim as u8);
2439 }
2440 if let Some(has_header) = opts.has_header {
2441 options = options.has_header(has_header);
2442 }
2443 let dataframe = self.context.read_csv(path, options).await?;
2444 Ok(self.make_sql_df("csv-read", dataframe))
2445 }
2446
2447 pub async fn read_json(&self, path: impl AsRef<Path>) -> SqlResult<SqlDataFrame> {
2449 let path = path.as_ref().to_string_lossy().into_owned();
2450 let dataframe = self
2451 .context
2452 .read_json(path, datafusion::prelude::JsonReadOptions::default())
2453 .await?;
2454 Ok(self.make_sql_df("json-read", dataframe))
2455 }
2456
2457 pub async fn read_delta(
2459 &self,
2460 path: impl AsRef<str>,
2461 version: Option<i64>,
2462 ) -> SqlResult<SqlDataFrame> {
2463 let path = path.as_ref();
2464 let base = path.replace(['/', '.', '-'], "_");
2465 let table = match version {
2466 Some(v) => format!("delta_{base}_v{v}"),
2467 None => format!("delta_{base}"),
2468 };
2469 lakehouse::register_delta_uri(&self.context, &table, path, version).await?;
2470 self.sql(format!("SELECT * FROM {table}")).await
2471 }
2472
2473 pub async fn read_hudi(
2475 &self,
2476 path: impl AsRef<str>,
2477 query_type: krishiv_connectors::lakehouse::HudiQueryType,
2478 begin_instant: Option<&str>,
2479 ) -> SqlResult<SqlDataFrame> {
2480 let path = path.as_ref();
2481 let table = format!("hudi_{}", path.replace(['/', '.', '-'], "_"));
2482 lakehouse::register_hudi_uri(&self.context, &table, path, query_type, begin_instant)
2483 .await?;
2484 self.sql(format!("SELECT * FROM {table}")).await
2485 }
2486
2487 pub fn sql<'a>(
2497 &'a self,
2498 query: impl AsRef<str> + Send + 'a,
2499 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = SqlResult<SqlDataFrame>> + Send + 'a>>
2500 {
2501 let query: String = query.as_ref().to_owned();
2502 Box::pin(self.sql_boxed_body(query))
2503 }
2504
2505 async fn sql_boxed_body(&self, query: String) -> SqlResult<SqlDataFrame> {
2507 let query = query.as_str();
2508 if query.trim().is_empty() {
2509 return Err(SqlError::EmptyQuery);
2510 }
2511
2512 let script = split_sql_statements(query);
2521 if script.len() > 1
2522 && let [setup @ .., last_stmt] = script.as_slice()
2523 {
2524 for stmt in setup {
2525 Box::pin(self.sql(stmt.as_str())).await?.collect().await?;
2529 }
2530 return Box::pin(self.sql(last_stmt.as_str())).await;
2531 }
2532
2533 {
2537 let current = self.udf_registry_version.load(Ordering::Acquire);
2538 let last = self.udf_last_synced_version.load(Ordering::Relaxed);
2539 if current != last {
2540 self.sync_all_udfs().await?;
2541 self.udf_last_synced_version
2542 .store(current, Ordering::Release);
2543 }
2544 }
2545
2546 if let Some(stmt) = introspection_sql::parse_introspection_statement(query)? {
2548 return match stmt {
2549 introspection_sql::IntrospectionStatement::Describe { table } => {
2550 let batch = introspection_sql::describe_table(&self.context, &table).await?;
2551 let describe_table_name = next_ephemeral_name("describe_result");
2552 lakehouse::register_scan_batches(
2553 &self.context,
2554 &describe_table_name,
2555 vec![batch],
2556 )
2557 .await?;
2558 let dataframe = self
2559 .context
2560 .sql(&format!("SELECT * FROM {describe_table_name}"))
2561 .await?;
2562 Ok(self.attach_query_metadata(self.make_sql_df("describe", dataframe), query))
2563 }
2564 introspection_sql::IntrospectionStatement::Explain { mode, query: inner } => {
2565 let text = introspection_sql::explain_query(&inner, mode)?;
2566 let batch = introspection_sql::explain_result_batch(&text)?;
2567 let explain_table = next_ephemeral_name("explain_result");
2568 lakehouse::register_scan_batches(&self.context, &explain_table, vec![batch])
2569 .await?;
2570 let dataframe = self
2571 .context
2572 .sql(&format!("SELECT * FROM {explain_table}"))
2573 .await?;
2574 Ok(self.attach_query_metadata(self.make_sql_df("explain", dataframe), query))
2575 }
2576 };
2577 }
2578
2579 if live_table::execute_live_table_ddl(&self.live_table_registry, query)?.is_some() {
2581 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2582 return Ok(self.attach_query_metadata(self.make_sql_df("live-table-ddl", empty), query));
2583 }
2584
2585 match incremental_view::execute_incremental_view_ddl(
2587 &self.incremental_view_registry,
2588 query,
2589 )? {
2590 Some(incremental_view::IncrementalViewResult::Refresh(_name)) => {
2591 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2594 return Ok(self.attach_query_metadata(
2595 self.make_sql_df("incremental-view-refresh", empty),
2596 query,
2597 ));
2598 }
2599 Some(_) => {
2600 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2601 return Ok(self.attach_query_metadata(
2602 self.make_sql_df("incremental-view-ddl", empty),
2603 query,
2604 ));
2605 }
2606 None => {}
2607 }
2608
2609 if let Some(ddl) = streaming_table_ddl::parse_create_streaming_table(query) {
2617 let _plan = streaming_window_plan::compile_streaming_window_sql(&ddl.query)?;
2618 return Err(SqlError::Unsupported {
2619 feature: format!(
2620 "CREATE STREAMING TABLE '{}' compiled to a continuous plan, but this session \
2621 has no streaming coordinator to run it; submit it via the continuous-stream \
2622 registration API or a cluster-attached session",
2623 ddl.name
2624 ),
2625 });
2626 }
2627
2628 if pipeline_ddl::execute_pipeline_ddl(&self.pipeline_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("pipeline-ddl", empty), query));
2634 }
2635
2636 let trimmed = query.trim();
2639 if trimmed
2640 .to_ascii_uppercase()
2641 .starts_with("SET SHUFFLE.PARTITIONS")
2642 {
2643 let value = trimmed.split('=').nth(1).map(|s| s.trim()).unwrap_or("");
2644 match value.parse::<u32>() {
2645 Ok(n) if n > 0 => {
2646 {
2647 let mut guard =
2648 self.shuffle_partitions
2649 .write()
2650 .map_err(|e| SqlError::DataFusion {
2651 message: e.to_string(),
2652 })?;
2653 *guard = Some(n);
2654 }
2655 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2656 return Ok(self.make_sql_df("set-shuffle-partitions", empty));
2657 }
2658 Ok(_) => {
2659 {
2660 let mut guard =
2661 self.shuffle_partitions
2662 .write()
2663 .map_err(|e| SqlError::DataFusion {
2664 message: e.to_string(),
2665 })?;
2666 *guard = None;
2667 }
2668 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2669 return Ok(self.make_sql_df("set-shuffle-partitions", empty));
2670 }
2671 Err(_) => {
2672 return Err(SqlError::DataFusion {
2673 message: format!(
2674 "invalid shuffle.partitions value '{value}'; expected a positive integer"
2675 ),
2676 });
2677 }
2678 }
2679 }
2680
2681 if let Some(result) = statement_completion::apply_use(&self.context, query) {
2685 result.map_err(|message| SqlError::DataFusion { message })?;
2686 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2687 return Ok(self.attach_query_metadata(self.make_sql_df("use", empty), query));
2688 }
2689 if let Some(rewrite) = statement_completion::rewrite_show_databases(query) {
2690 let dataframe = self.context.sql(&rewrite).await?;
2691 return Ok(
2692 self.attach_query_metadata(self.make_sql_df("show-databases", dataframe), query)
2693 );
2694 }
2695
2696 if create_function_ddl::is_create_function_returns_table(query) {
2701 let ddl = create_function_ddl::parse_create_function(query)
2702 .map_err(|message| SqlError::InvalidTableFunction { message })?;
2703 if ddl.language.as_deref() != Some("sql") {
2704 return Err(SqlError::Unsupported {
2705 feature: format!(
2706 "CREATE FUNCTION '{}' uses language {:?}; only LANGUAGE SQL AS '...' \
2707 table functions are executable",
2708 ddl.function_name, ddl.language
2709 ),
2710 });
2711 }
2712 let body = ddl
2713 .body
2714 .as_deref()
2715 .filter(|body| !body.trim().is_empty())
2716 .ok_or_else(|| SqlError::InvalidTableFunction {
2717 message: format!(
2718 "SQL table function '{}' requires a non-empty AS body",
2719 ddl.function_name
2720 ),
2721 })?;
2722 let fields: Vec<_> = ddl
2723 .return_columns
2724 .iter()
2725 .map(|column| {
2726 arrow::datatypes::Field::new(&column.name, column.data_type.clone(), true)
2727 })
2728 .collect();
2729 let schema = arrow::datatypes::Schema::new(fields);
2730 let udf: std::sync::Arc<dyn krishiv_plan::udf::TableUdf> = std::sync::Arc::new(
2731 create_function_ddl::SqlBodyTableUdf::try_new(
2732 &ddl.function_name,
2733 schema,
2734 body,
2735 ddl.arguments.len(),
2736 std::sync::Arc::new(self.context.clone()),
2737 )
2738 .map_err(|error| SqlError::InvalidTableFunction {
2739 message: error.to_string(),
2740 })?,
2741 );
2742 if let Some(registry) = &self.udf_registry {
2743 let mut guard = registry.write().map_err(|e| SqlError::DataFusion {
2744 message: e.to_string(),
2745 })?;
2746 guard.register_table(std::sync::Arc::clone(&udf));
2747 }
2748 udf::register_single_table_udf(&self.context, std::sync::Arc::clone(&udf))
2749 .map_err(SqlError::from)?;
2750 let empty = self.context.sql("SELECT 1 WHERE FALSE").await?;
2751 return Ok(
2752 self.attach_query_metadata(self.make_sql_df("create-function", empty), query)
2753 );
2754 }
2755
2756 if query
2757 .trim_start()
2758 .to_ascii_uppercase()
2759 .starts_with("MERGE INTO")
2760 {
2761 let batches = lakehouse::execute_merge_sql(&self.context, query).await?;
2762 let merge_table = next_ephemeral_name("merge_result");
2763 lakehouse::register_scan_batches(&self.context, &merge_table, batches).await?;
2764 let dataframe = self
2765 .context
2766 .sql(&format!("SELECT * FROM {merge_table}"))
2767 .await?;
2768 return Ok(self.attach_query_metadata(self.make_sql_df("merge", dataframe), query));
2769 }
2770
2771 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2780 if trimmed.to_ascii_uppercase().starts_with("CREATE ")
2781 && let Some(parsed_ctas) = parse_ctas(trimmed)
2782 {
2783 let resolved = self.resolve_iceberg_table(&parsed_ctas.table_ref);
2784 if resolved.is_none() && !parsed_ctas.partition_by.is_empty() {
2787 return Err(SqlError::DataFusion {
2788 message: format!(
2789 "PARTITIONED BY requires an Iceberg catalog table; `{}` does not \
2790 resolve to a registered Iceberg catalog",
2791 parsed_ctas.table_ref
2792 ),
2793 });
2794 }
2795 if let Some((iceberg_catalog, table_ident)) = resolved {
2796 return self
2797 .execute_iceberg_ctas(iceberg_catalog, table_ident, parsed_ctas, query)
2798 .await;
2799 }
2800 }
2801
2802 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2805 if trimmed.to_ascii_uppercase().starts_with("CALL SYSTEM.") {
2806 let result = self.dispatch_call_system(trimmed).await?;
2807 let call_table = next_ephemeral_name("call_result");
2808 lakehouse::register_scan_batches(&self.context, &call_table, vec![result]).await?;
2809 let dataframe = self
2810 .context
2811 .sql(&format!("SELECT * FROM {call_table}"))
2812 .await?;
2813 return Ok(self.attach_query_metadata(self.make_sql_df("call", dataframe), query));
2814 }
2815
2816 if trimmed
2822 .get(..14)
2823 .is_some_and(|p| p.eq_ignore_ascii_case("ANALYZE TABLE "))
2824 {
2825 let result = self.dispatch_analyze_table(trimmed).await?;
2826 let res_table = next_ephemeral_name("analyze_result");
2827 lakehouse::register_scan_batches(&self.context, &res_table, vec![result]).await?;
2828 let dataframe = self
2829 .context
2830 .sql(&format!("SELECT * FROM {res_table}"))
2831 .await?;
2832 return Ok(self.attach_query_metadata(self.make_sql_df("analyze", dataframe), query));
2833 }
2834
2835 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2839 if trimmed.to_ascii_uppercase().starts_with("DELETE FROM ")
2840 && let Some((table_ref, predicate)) = parse_dml_delete(trimmed)
2841 && let Some((iceberg_catalog, table_ident)) = self.resolve_iceberg_table(&table_ref)
2842 {
2843 use arrow::array::{ArrayRef, Int64Array};
2844 use arrow::datatypes::{DataType, Field, Schema};
2845 let (deleted, _) = krishiv_connectors::lakehouse::dml::iceberg_delete_where(
2846 iceberg_catalog,
2847 &table_ident,
2848 &predicate,
2849 &self.context,
2850 )
2851 .await
2852 .map_err(|e| SqlError::DataFusion {
2853 message: e.to_string(),
2854 })?;
2855 self.adjust_table_row_count_stat(&table_ref, -(deleted as i64));
2857 let schema = Arc::new(Schema::new(vec![Field::new(
2858 "deleted_rows",
2859 DataType::Int64,
2860 false,
2861 )]));
2862 let array: ArrayRef = Arc::new(Int64Array::from(vec![deleted as i64]));
2863 let batch =
2864 RecordBatch::try_new(schema, vec![array]).map_err(|e| SqlError::DataFusion {
2865 message: e.to_string(),
2866 })?;
2867 let res_table = next_ephemeral_name("delete_result");
2868 lakehouse::register_scan_batches(&self.context, &res_table, vec![batch]).await?;
2869 let dataframe = self
2870 .context
2871 .sql(&format!("SELECT * FROM {res_table}"))
2872 .await?;
2873 return Ok(self.attach_query_metadata(self.make_sql_df("delete", dataframe), query));
2874 }
2875
2876 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2878 if trimmed.to_ascii_uppercase().starts_with("UPDATE ")
2879 && let Some(parsed) = parse_dml_update(trimmed)
2880 && let Some((iceberg_catalog, table_ident)) =
2881 self.resolve_iceberg_table(&parsed.table_ref)
2882 {
2883 use arrow::array::{ArrayRef, Int64Array};
2884 use arrow::datatypes::{DataType, Field, Schema};
2885 let borrowed: Vec<(&str, &str)> = parsed
2886 .assignments
2887 .iter()
2888 .map(|(c, e)| (c.as_str(), e.as_str()))
2889 .collect();
2890 let pred = parsed.predicate.as_deref();
2891 let (updated, _) = krishiv_connectors::lakehouse::dml::iceberg_update_where(
2892 iceberg_catalog,
2893 &table_ident,
2894 &borrowed,
2895 pred,
2896 &self.context,
2897 )
2898 .await
2899 .map_err(|e| SqlError::DataFusion {
2900 message: e.to_string(),
2901 })?;
2902 let schema = Arc::new(Schema::new(vec![Field::new(
2903 "updated_rows",
2904 DataType::Int64,
2905 false,
2906 )]));
2907 let array: ArrayRef = Arc::new(Int64Array::from(vec![updated as i64]));
2908 let batch =
2909 RecordBatch::try_new(schema, vec![array]).map_err(|e| SqlError::DataFusion {
2910 message: e.to_string(),
2911 })?;
2912 let res_table = next_ephemeral_name("update_result");
2913 lakehouse::register_scan_batches(&self.context, &res_table, vec![batch]).await?;
2914 let dataframe = self
2915 .context
2916 .sql(&format!("SELECT * FROM {res_table}"))
2917 .await?;
2918 return Ok(self.attach_query_metadata(self.make_sql_df("update", dataframe), query));
2919 }
2920
2921 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
2931 if trimmed.to_ascii_uppercase().starts_with("INSERT ")
2932 && let Some(parsed) = parse_dml_insert(trimmed)
2933 && parsed.columns.is_empty()
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 source_df = self.context.sql(&parsed.inner_query).await?;
2940 let stream = source_df
2941 .execute_stream()
2942 .await
2943 .map_err(|e| SqlError::DataFusion {
2944 message: e.to_string(),
2945 })?;
2946 let report = krishiv_connectors::lakehouse::dml::iceberg_append_into(
2947 iceberg_catalog,
2948 &table_ident,
2949 stream,
2950 )
2951 .await
2952 .map_err(|e| SqlError::DataFusion {
2953 message: e.to_string(),
2954 })?;
2955 self.adjust_table_row_count_stat(&parsed.table_ref, report.rows as i64);
2957 let schema = Arc::new(Schema::new(vec![Field::new(
2958 "inserted_rows",
2959 DataType::Int64,
2960 false,
2961 )]));
2962 let array: ArrayRef = Arc::new(Int64Array::from(vec![report.rows as i64]));
2963 let batch =
2964 RecordBatch::try_new(schema, vec![array]).map_err(|e| SqlError::DataFusion {
2965 message: e.to_string(),
2966 })?;
2967 let res_table = next_ephemeral_name("insert_result");
2968 lakehouse::register_scan_batches(&self.context, &res_table, vec![batch]).await?;
2969 let dataframe = self
2970 .context
2971 .sql(&format!("SELECT * FROM {res_table}"))
2972 .await?;
2973 return Ok(self.attach_query_metadata(self.make_sql_df("insert", dataframe), query));
2974 }
2975
2976 if query.to_ascii_uppercase().contains(" MATCH_RECOGNIZE ")
2980 && let Some(stmt) = cep_sql::parse_match_recognize(query)?
2981 {
2982 let is_streaming = self.is_streaming_source(&stmt.source_table);
2983 let streaming_limit = streaming_match_recognize_limit_from_env();
2991 let source_sql = if is_streaming {
2992 format!(
2993 "SELECT * FROM {} LIMIT {}",
2994 stmt.source_table, streaming_limit
2995 )
2996 } else {
2997 format!("SELECT * FROM {}", stmt.source_table)
2998 };
2999 let source_df = self.context.sql(&source_sql).await?;
3000 let source_batches = source_df.collect().await?;
3001 if is_streaming {
3002 tracing::warn!(
3003 source = %stmt.source_table,
3004 limit = streaming_limit,
3005 collected_rows = source_batches.iter().map(|b| b.num_rows()).sum::<usize>(),
3006 "MATCH_RECOGNIZE executed against a streaming source under \
3007 bounded materialisation; results only cover the first {0} rows \
3008 of the source. Set KRISHIV_MATCH_RECOGNIZE_STREAMING_LIMIT to a \
3009 larger value if your executor has the memory budget.",
3010 streaming_limit
3011 );
3012 }
3013 let results = cep_sql::execute_match_recognize(stmt, &source_batches)?;
3014 let cep_table = next_ephemeral_name("cep_result");
3015 lakehouse::register_scan_batches(&self.context, &cep_table, results).await?;
3016 let dataframe = self
3017 .context
3018 .sql(&format!("SELECT * FROM {cep_table}"))
3019 .await?;
3020 return Ok(self.attach_query_metadata(self.make_sql_df("cep", dataframe), query));
3021 }
3022
3023 let query = &pivot_sql::rewrite_pivot_unpivot(query)?;
3026
3027 let query = &streaming_tvf::rewrite_window_tvfs(query);
3029
3030 let (rewritten, as_ofs) =
3031 lakehouse::preprocess_as_of_sql(query).unwrap_or_else(|_| (query.to_string(), vec![]));
3032 lakehouse::apply_as_of_refs(&self.context, &as_ofs).await?;
3033
3034 let can_cache = as_ofs.is_empty();
3041 let shuffle_override = self
3042 .shuffle_partitions
3043 .read()
3044 .map(|g| *g)
3045 .unwrap_or_else(|e| *e.into_inner());
3046 if can_cache {
3047 let cached_plan: Option<datafusion::logical_expr::LogicalPlan> = self
3049 .plan_cache
3050 .lock()
3051 .unwrap_or_else(|e| e.into_inner())
3052 .get(&rewritten)
3053 .cloned();
3054 if let Some(plan) = cached_plan {
3055 let dataframe = self.context.execute_logical_plan(plan).await?;
3056 return Ok(self.attach_query_metadata(
3057 self.make_sql_df("sql-query", dataframe)
3058 .with_shuffle_partitions(shuffle_override),
3059 &rewritten,
3060 ));
3061 }
3062 }
3063
3064 if let Some(location) = extract_create_external_table_location(&rewritten) {
3073 self.register_s3_object_store_for_warehouse(&location)
3074 .map_err(|message| SqlError::DataFusion { message })?;
3075 }
3076
3077 let dataframe = self.context.sql(&rewritten).await?;
3078
3079 if let Some(table_name) = extract_create_external_table_name(&rewritten)
3083 && !table_name.is_empty()
3084 && let Ok(provider) = self.context.table_provider(&table_name).await
3085 {
3086 let maybe_rows = provider
3087 .statistics()
3088 .and_then(|s| s.num_rows.get_value().copied());
3089 if let Some(n) = maybe_rows
3090 && let Ok(mut counts) = self.table_row_counts.write()
3091 {
3092 counts.entry(table_name).or_insert(n as u64);
3093 }
3094 }
3095
3096 if can_cache {
3098 let plan = dataframe.logical_plan().clone();
3099 match self.plan_cache.lock() {
3100 Ok(mut cache) => cache.insert(rewritten.clone(), plan),
3101 Err(poisoned) => poisoned.into_inner().insert(rewritten.clone(), plan),
3102 }
3103 }
3104
3105 Ok(self.attach_query_metadata(
3106 self.make_sql_df("sql-query", dataframe)
3107 .with_shuffle_partitions(shuffle_override),
3108 &rewritten,
3109 ))
3110 }
3111
3112 pub async fn execute_with_timeout(
3119 &self,
3120 query: impl AsRef<str> + Send,
3121 timeout_ms: u64,
3122 ) -> SqlResult<SqlDataFrame> {
3123 let timeout = std::time::Duration::from_millis(timeout_ms);
3124 tokio::time::timeout(timeout, self.sql(query))
3125 .await
3126 .map_err(|_| SqlError::Timeout { timeout_ms })?
3127 }
3128
3129 pub async fn execute_with_operation_id(
3136 &self,
3137 operation_id: u64,
3138 query: impl AsRef<str> + Send,
3139 cancelled_ids: &OperationRegistry,
3140 ) -> SqlResult<TaggedQueryResult> {
3141 if cancelled_ids.is_cancelled(operation_id) {
3142 return Err(SqlError::OperationCancelled { operation_id });
3143 }
3144 let df = self.sql(query).await?;
3145 Ok(TaggedQueryResult {
3146 operation_id,
3147 inner: df,
3148 })
3149 }
3150
3151 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
3157 fn resolve_iceberg_table(
3158 &self,
3159 table_ref: &str,
3160 ) -> Option<(Arc<dyn iceberg::Catalog + Send + Sync>, iceberg::TableIdent)> {
3161 let parts: Vec<&str> = table_ref.splitn(3, '.').collect();
3162 let (catalog_arc, ns_str, table_str) = {
3163 let guard = self
3164 .iceberg_catalogs
3165 .read()
3166 .unwrap_or_else(|e| e.into_inner());
3167 if guard.is_empty() {
3168 return None;
3169 }
3170 match parts.len() {
3171 2 => {
3172 let (cat, _) = guard.first()?;
3173 (Arc::clone(cat), *parts.first()?, *parts.get(1)?)
3174 }
3175 3 => {
3176 let cat_name = parts.first().copied()?;
3177 let (cat, _) = guard.iter().find(|(_, n)| n == cat_name)?;
3178 (Arc::clone(cat), *parts.get(1)?, *parts.get(2)?)
3179 }
3180 _ => return None,
3181 }
3182 };
3183 let ns = iceberg::NamespaceIdent::from_vec(vec![ns_str.to_string()]).ok()?;
3184 let ident = iceberg::TableIdent::new(ns, table_str.to_string());
3185 Some((catalog_arc.as_iceberg(), ident))
3186 }
3187
3188 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
3194 async fn execute_iceberg_ctas(
3195 &self,
3196 iceberg_catalog: Arc<dyn iceberg::Catalog + Send + Sync>,
3197 table_ident: iceberg::TableIdent,
3198 parsed_ctas: ParsedCtas,
3199 query: &str,
3200 ) -> SqlResult<SqlDataFrame> {
3201 use arrow::array::{ArrayRef, Int64Array};
3202 use arrow::datatypes::{DataType, Field, Schema};
3203 use krishiv_connectors::lakehouse::partitioned_write::parse_partition_transform;
3204
3205 let partition_by = parsed_ctas
3206 .partition_by
3207 .iter()
3208 .map(|item| parse_partition_transform(item))
3209 .collect::<Result<Vec<_>, _>>()
3210 .map_err(|e| SqlError::DataFusion {
3211 message: e.to_string(),
3212 })?;
3213
3214 let dataframe = self.context.sql(&parsed_ctas.inner_query).await?;
3215 let stream = dataframe
3216 .execute_stream()
3217 .await
3218 .map_err(|e| SqlError::DataFusion {
3219 message: e.to_string(),
3220 })?;
3221 let report = krishiv_connectors::lakehouse::dml::land_ctas(
3222 iceberg_catalog,
3223 &table_ident,
3224 parsed_ctas.or_replace,
3225 &partition_by,
3226 stream,
3227 )
3228 .await
3229 .map_err(|e| SqlError::DataFusion {
3230 message: e.to_string(),
3231 })?;
3232 self.invalidate_plan_cache();
3234 self.record_table_row_count_stat(&parsed_ctas.table_ref, report.rows as u64);
3236
3237 let schema = Arc::new(Schema::new(vec![
3238 Field::new("rows_written", DataType::Int64, false),
3239 Field::new("bytes_written", DataType::Int64, false),
3240 Field::new("data_files", DataType::Int64, false),
3241 Field::new("snapshot_id", DataType::Int64, false),
3242 ]));
3243 let columns: Vec<ArrayRef> = vec![
3244 Arc::new(Int64Array::from(vec![report.rows as i64])),
3245 Arc::new(Int64Array::from(vec![report.bytes as i64])),
3246 Arc::new(Int64Array::from(vec![report.data_files as i64])),
3247 Arc::new(Int64Array::from(vec![report.snapshot_id])),
3248 ];
3249 let batch = RecordBatch::try_new(schema, columns).map_err(|e| SqlError::DataFusion {
3250 message: e.to_string(),
3251 })?;
3252 let res_table = next_ephemeral_name("ctas_result");
3253 lakehouse::register_scan_batches(&self.context, &res_table, vec![batch]).await?;
3254 let dataframe = self
3255 .context
3256 .sql(&format!("SELECT * FROM {res_table}"))
3257 .await?;
3258 Ok(self.attach_query_metadata(self.make_sql_df("ctas", dataframe), query))
3259 }
3260
3261 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
3266 fn record_table_row_count_stat(&self, table_ref: &str, row_count: u64) {
3267 let registry = krishiv_plan::optimizer::global_table_stats();
3268 let mut names = vec![table_ref];
3269 let bare = table_ref.rsplit('.').next().unwrap_or(table_ref);
3270 if bare != table_ref {
3271 names.push(bare);
3272 }
3273 for name in &names {
3274 let mut stats = registry
3275 .get(name)
3276 .unwrap_or_else(|| krishiv_plan::optimizer::TableCboStats::new(*name));
3277 stats.row_count = Some(row_count);
3278 registry.put(stats);
3279 }
3280 if let Ok(mut counts) = self.table_row_counts.write() {
3281 for name in &names {
3282 counts.insert((*name).to_owned(), row_count);
3283 }
3284 }
3285 }
3286
3287 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
3292 fn adjust_table_row_count_stat(&self, table_ref: &str, delta: i64) {
3293 let registry = krishiv_plan::optimizer::global_table_stats();
3294 let mut names = vec![table_ref];
3295 let bare = table_ref.rsplit('.').next().unwrap_or(table_ref);
3296 if bare != table_ref {
3297 names.push(bare);
3298 }
3299 for name in &names {
3300 if let Some(mut stats) = registry.get(name)
3301 && let Some(current) = stats.row_count
3302 {
3303 stats.row_count = Some(current.saturating_add_signed(delta));
3304 registry.put(stats);
3305 }
3306 }
3307 if let Ok(mut counts) = self.table_row_counts.write() {
3308 for name in &names {
3309 if let Some(current) = counts.get(*name).copied() {
3310 counts.insert((*name).to_owned(), current.saturating_add_signed(delta));
3311 }
3312 }
3313 }
3314 }
3315
3316 async fn dispatch_analyze_table(&self, stmt: &str) -> SqlResult<RecordBatch> {
3327 use arrow::array::{ArrayRef, Int64Array, StringArray};
3328 use arrow::datatypes::{DataType, Field, Schema};
3329
3330 let rest = stmt
3331 .get(14..)
3332 .unwrap_or("")
3333 .trim()
3334 .trim_end_matches(';')
3335 .trim();
3336 let (table_ref, tail) = match rest.split_once(char::is_whitespace) {
3337 Some((t, tail)) => (t.trim(), tail.trim()),
3338 None => (rest, ""),
3339 };
3340 if table_ref.is_empty() {
3341 return Err(SqlError::DataFusion {
3342 message: String::from("ANALYZE TABLE: table reference is required"),
3343 });
3344 }
3345 let mut tail = tail;
3347 if tail
3348 .get(..18)
3349 .is_some_and(|p| p.eq_ignore_ascii_case("COMPUTE STATISTICS"))
3350 {
3351 tail = tail.get(18..).unwrap_or("").trim();
3352 }
3353 let columns: Vec<String> = if tail
3354 .get(..11)
3355 .is_some_and(|p| p.eq_ignore_ascii_case("FOR COLUMNS"))
3356 {
3357 tail.get(11..)
3358 .unwrap_or("")
3359 .trim()
3360 .trim_start_matches('(')
3361 .trim_end_matches(')')
3362 .split(',')
3363 .map(|c| c.trim().trim_matches('"').to_owned())
3364 .filter(|c| !c.is_empty())
3365 .collect()
3366 } else if tail.is_empty() {
3367 Vec::new()
3368 } else {
3369 return Err(SqlError::DataFusion {
3370 message: format!("ANALYZE TABLE: unexpected trailing clause: {tail}"),
3371 });
3372 };
3373
3374 let mut projections = vec![String::from("count(*)")];
3376 for c in &columns {
3377 projections.push(format!("approx_distinct(\"{c}\")"));
3378 projections.push(format!("min(\"{c}\")"));
3379 projections.push(format!("max(\"{c}\")"));
3380 projections.push(format!("count(\"{c}\")"));
3381 }
3382 let scan_sql = format!("SELECT {} FROM {table_ref}", projections.join(", "));
3383 let batches = self.context.sql(&scan_sql).await?.collect().await?;
3384 let row =
3385 batches
3386 .iter()
3387 .find(|b| b.num_rows() > 0)
3388 .ok_or_else(|| SqlError::DataFusion {
3389 message: format!("ANALYZE TABLE {table_ref}: aggregation returned no rows"),
3390 })?;
3391 let cell_string = |col: usize| -> Option<String> {
3392 let column = row.columns().get(col)?;
3393 if column.is_null(0) {
3394 return None;
3395 }
3396 arrow::util::display::array_value_to_string(column, 0).ok()
3397 };
3398 let cell_u64 = |col: usize| -> Option<u64> { cell_string(col)?.parse().ok() };
3399 let row_count = cell_u64(0).ok_or_else(|| SqlError::DataFusion {
3400 message: format!("ANALYZE TABLE {table_ref}: COUNT(*) unreadable"),
3401 })?;
3402
3403 let mut column_stats = Vec::with_capacity(columns.len());
3404 for (i, name) in columns.iter().enumerate() {
3405 let base = 1 + i * 4;
3406 let non_null = cell_u64(base + 3);
3407 column_stats.push(krishiv_plan::optimizer::ColumnCboStats {
3408 name: name.clone(),
3409 ndv: cell_u64(base),
3410 min: cell_string(base + 1),
3411 max: cell_string(base + 2),
3412 null_count: non_null.map(|n| row_count.saturating_sub(n)),
3413 });
3414 }
3415
3416 let avg_row_bytes = match self.context.table_provider(table_ref).await {
3418 Ok(provider) => provider.statistics().and_then(|s| {
3419 let rows = s.num_rows.get_value().copied()?;
3420 let bytes = s.total_byte_size.get_value().copied()?;
3421 (rows > 0).then(|| (bytes / rows) as u64)
3422 }),
3423 Err(_) => None,
3424 };
3425
3426 let mut stats =
3427 krishiv_plan::optimizer::TableCboStats::new(table_ref).with_row_count(row_count);
3428 if let Some(bytes) = avg_row_bytes {
3429 stats = stats.with_avg_row_bytes(bytes);
3430 }
3431 if let Some(max_ndv) = column_stats.iter().filter_map(|c| c.ndv).max() {
3432 stats = stats.with_ndv(max_ndv);
3434 }
3435 stats.columns = column_stats;
3436 let registry = krishiv_plan::optimizer::global_table_stats();
3437 let bare = table_ref.rsplit('.').next().unwrap_or(table_ref);
3440 if bare != table_ref {
3441 let mut bare_stats = stats.clone();
3442 bare_stats.table = bare.to_owned();
3443 registry.put(bare_stats);
3444 }
3445 let analyzed_columns = stats.columns.len();
3446 registry.put(stats);
3447 if let Ok(mut counts) = self.table_row_counts.write() {
3448 counts.insert(table_ref.to_owned(), row_count);
3449 if bare != table_ref {
3450 counts.insert(bare.to_owned(), row_count);
3451 }
3452 }
3453 self.invalidate_plan_cache();
3454
3455 let schema = Arc::new(Schema::new(vec![
3456 Field::new("table_name", DataType::Utf8, false),
3457 Field::new("row_count", DataType::Int64, false),
3458 Field::new("avg_row_bytes", DataType::Int64, true),
3459 Field::new("columns_analyzed", DataType::Int64, false),
3460 ]));
3461 let columns_out: Vec<ArrayRef> = vec![
3462 Arc::new(StringArray::from(vec![table_ref.to_owned()])),
3463 Arc::new(Int64Array::from(vec![row_count as i64])),
3464 Arc::new(Int64Array::from(vec![avg_row_bytes.map(|b| b as i64)])),
3465 Arc::new(Int64Array::from(vec![analyzed_columns as i64])),
3466 ];
3467 RecordBatch::try_new(schema, columns_out).map_err(|e| SqlError::DataFusion {
3468 message: e.to_string(),
3469 })
3470 }
3471
3472 #[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
3475 async fn dispatch_call_system(&self, stmt: &str) -> SqlResult<RecordBatch> {
3476 use arrow::array::{ArrayRef, Int64Array};
3477 use arrow::datatypes::{DataType, Field, Schema};
3478
3479 let upper = stmt.to_ascii_uppercase();
3480 const PREFIX: &str = "CALL SYSTEM.";
3481 let upper_after = &upper[PREFIX.len()..];
3482 let orig_after = &stmt[PREFIX.len()..];
3483
3484 let paren = upper_after.find('(').ok_or_else(|| SqlError::DataFusion {
3485 message: format!("CALL: missing '(' in: {stmt}"),
3486 })?;
3487 let proc_name = upper_after[..paren].trim();
3488
3489 let args_raw = orig_after[paren + 1..]
3490 .trim_end_matches(';')
3491 .trim()
3492 .trim_end_matches(')')
3493 .trim();
3494 let args = call_args_from_str(args_raw);
3495
3496 let iceberg_catalog = {
3497 let guard = self
3498 .iceberg_catalogs
3499 .read()
3500 .unwrap_or_else(|e| e.into_inner());
3501 guard
3502 .first()
3503 .ok_or_else(|| SqlError::DataFusion {
3504 message: "CALL system: no Iceberg catalog registered".to_string(),
3505 })?
3506 .0
3507 .as_iceberg()
3508 };
3509
3510 let table_ref = args.first().ok_or_else(|| SqlError::DataFusion {
3511 message: format!("CALL {proc_name}: table reference argument is required"),
3512 })?;
3513 let table_ident = iceberg_table_ident(table_ref)?;
3514
3515 if proc_name == "MAINTAIN_TABLE" {
3519 let older_than = parse_call_duration(args.get(1).map_or("7 days", |s| s.as_str()))?;
3520 let target_bytes = args
3521 .get(2)
3522 .and_then(|s| s.parse::<u64>().ok())
3523 .unwrap_or(128 * 1024 * 1024);
3524 let retain_last = args
3525 .get(3)
3526 .and_then(|s| s.parse::<usize>().ok())
3527 .unwrap_or(1);
3528 let report = krishiv_connectors::lakehouse::maintenance::maintain_table(
3529 iceberg_catalog,
3530 &table_ident,
3531 target_bytes,
3532 older_than,
3533 retain_last,
3534 )
3535 .await
3536 .map_err(|e| SqlError::DataFusion {
3537 message: e.to_string(),
3538 })?;
3539 let schema = Arc::new(Schema::new(vec![
3540 Field::new("compacted_files", DataType::Int64, false),
3541 Field::new("expired_snapshots", DataType::Int64, false),
3542 Field::new("removed_orphans", DataType::Int64, false),
3543 ]));
3544 let columns: Vec<ArrayRef> = vec![
3545 Arc::new(Int64Array::from(vec![report.compacted_files as i64])),
3546 Arc::new(Int64Array::from(vec![report.expired_snapshots as i64])),
3547 Arc::new(Int64Array::from(vec![report.removed_orphans as i64])),
3548 ];
3549 return RecordBatch::try_new(schema, columns).map_err(|e| SqlError::DataFusion {
3550 message: e.to_string(),
3551 });
3552 }
3553
3554 let count: i64 = match proc_name {
3555 "EXPIRE_SNAPSHOTS" => {
3556 let dur_s = args.get(1).ok_or_else(|| SqlError::DataFusion {
3557 message: "CALL expire_snapshots: duration argument is required".to_string(),
3558 })?;
3559 let older_than = parse_call_duration(dur_s)?;
3560 let retain_last = args
3561 .get(2)
3562 .and_then(|s| s.parse::<usize>().ok())
3563 .unwrap_or(1);
3564 krishiv_connectors::lakehouse::maintenance::expire_snapshots(
3565 iceberg_catalog,
3566 &table_ident,
3567 older_than,
3568 retain_last,
3569 )
3570 .await
3571 .map_err(|e| SqlError::DataFusion {
3572 message: e.to_string(),
3573 })? as i64
3574 }
3575 "REMOVE_ORPHAN_FILES" => {
3576 let dur_s = args.get(1).ok_or_else(|| SqlError::DataFusion {
3577 message: "CALL remove_orphan_files: duration argument is required".to_string(),
3578 })?;
3579 let older_than = parse_call_duration(dur_s)?;
3580 krishiv_connectors::lakehouse::maintenance::remove_orphan_files(
3581 iceberg_catalog,
3582 &table_ident,
3583 older_than,
3584 )
3585 .await
3586 .map_err(|e| SqlError::DataFusion {
3587 message: e.to_string(),
3588 })? as i64
3589 }
3590 "COMPACT_DATA_FILES" => {
3591 let target_bytes = args
3592 .get(1)
3593 .and_then(|s| s.parse::<u64>().ok())
3594 .unwrap_or(128 * 1024 * 1024);
3595 krishiv_connectors::lakehouse::maintenance::compact_data_files(
3596 iceberg_catalog,
3597 &table_ident,
3598 target_bytes,
3599 )
3600 .await
3601 .map_err(|e| SqlError::DataFusion {
3602 message: e.to_string(),
3603 })? as i64
3604 }
3605 other => {
3606 return Err(SqlError::Unsupported {
3607 feature: format!("CALL system.{other}: unknown procedure"),
3608 });
3609 }
3610 };
3611
3612 let col = match proc_name {
3613 "EXPIRE_SNAPSHOTS" => "expired_snapshots",
3614 "REMOVE_ORPHAN_FILES" => "removed_files",
3615 "COMPACT_DATA_FILES" => "rewritten_files",
3616 _ => "result",
3617 };
3618 let schema = Arc::new(Schema::new(vec![Field::new(col, DataType::Int64, false)]));
3619 let array: ArrayRef = Arc::new(Int64Array::from(vec![count]));
3620 RecordBatch::try_new(schema, vec![array]).map_err(|e| SqlError::DataFusion {
3621 message: e.to_string(),
3622 })
3623 }
3624}
3625
3626pub struct TaggedQueryResult {
3628 pub operation_id: u64,
3630 pub inner: SqlDataFrame,
3632}
3633
3634#[derive(Clone, Default)]
3640pub struct OperationRegistry {
3641 cancelled: Arc<std::sync::RwLock<std::collections::HashSet<u64>>>,
3642 progress: Arc<std::sync::RwLock<std::collections::HashMap<u64, (u64, u64)>>>,
3643}
3644
3645impl OperationRegistry {
3646 pub fn new() -> Self {
3648 Self::default()
3649 }
3650
3651 pub fn cancel(&self, operation_id: u64) {
3655 if let Ok(mut ids) = self.cancelled.write() {
3656 ids.insert(operation_id);
3657 }
3658 }
3659
3660 pub fn is_cancelled(&self, operation_id: u64) -> bool {
3662 self.cancelled
3663 .read()
3664 .map(|ids| ids.contains(&operation_id))
3665 .unwrap_or(false)
3666 }
3667
3668 pub fn remove(&self, operation_id: u64) {
3670 if let Ok(mut ids) = self.cancelled.write() {
3671 ids.remove(&operation_id);
3672 }
3673 if let Ok(mut progress) = self.progress.write() {
3674 progress.remove(&operation_id);
3675 }
3676 }
3677
3678 pub fn update_progress(&self, operation_id: u64, rows_scanned: u64, rows_emitted: u64) {
3680 if let Ok(mut progress) = self.progress.write() {
3681 progress.insert(operation_id, (rows_scanned, rows_emitted));
3682 }
3683 }
3684
3685 pub fn progress(&self, operation_id: u64) -> Option<(u64, u64)> {
3687 self.progress
3688 .read()
3689 .ok()
3690 .and_then(|progress| progress.get(&operation_id).copied())
3691 }
3692
3693 pub fn cancelled_ids(&self) -> Vec<u64> {
3695 self.cancelled
3696 .read()
3697 .map(|ids| ids.iter().copied().collect())
3698 .unwrap_or_default()
3699 }
3700}
3701
3702pub(crate) fn extract_create_external_table_name(query: &str) -> Option<String> {
3707 use datafusion::sql::parser::{DFParser, Statement as DFStatement};
3708 let mut stmts = DFParser::parse_sql(query).ok()?;
3709 match stmts.pop_front()? {
3710 DFStatement::CreateExternalTable(create) => Some(create.name.to_string()),
3711 _ => None,
3712 }
3713}
3714
3715pub(crate) fn extract_create_external_table_location(query: &str) -> Option<String> {
3723 use datafusion::sql::parser::{DFParser, Statement as DFStatement};
3724 let mut stmts = DFParser::parse_sql(query).ok()?;
3725 match stmts.pop_front()? {
3726 DFStatement::CreateExternalTable(create) => Some(create.location),
3727 _ => None,
3728 }
3729}
3730
3731pub enum GroupingMode<'a> {
3739 Sets(Vec<Vec<&'a krishiv_plan::expression::Expr>>),
3740 Cube(Vec<&'a krishiv_plan::expression::Expr>),
3741 Rollup(Vec<&'a krishiv_plan::expression::Expr>),
3742}
3743
3744#[async_trait::async_trait]
3745pub trait KrishivDataFrameOps: Send + Sync {
3746 async fn collect(&self) -> SqlResult<Vec<RecordBatch>>;
3748 async fn collect_with_stats(&self) -> SqlResult<(Vec<RecordBatch>, SqlExecutionStats)>;
3750 async fn explain(&self) -> SqlResult<String>;
3752
3753 async fn explain_analyze(&self) -> SqlResult<String> {
3759 Err(SqlError::DataFusion {
3760 message: String::from("EXPLAIN ANALYZE is not supported for this dataframe backend"),
3761 })
3762 }
3763 fn explain_logical(&self) -> String;
3765 fn krishiv_logical_plan(&self) -> LogicalPlan;
3767 fn query(&self) -> Option<&str>;
3769 fn to_sql(&self) -> SqlResult<String> {
3773 Err(SqlError::Unsupported {
3774 feature: "to_sql (plan unparsing) is not supported for this DataFrame".into(),
3775 })
3776 }
3777 async fn execute_stream(&self) -> SqlResult<SqlStream>;
3779
3780 fn schema(&self) -> SchemaRef;
3784
3785 async fn select(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3787
3788 async fn select_exprs(
3790 &self,
3791 expressions: &[&krishiv_plan::expression::Expr],
3792 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3793
3794 async fn unnest_columns(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3798
3799 async fn aggregate(
3801 &self,
3802 group_exprs: &[&krishiv_plan::expression::Expr],
3803 aggregate_exprs: &[&krishiv_plan::expression::Expr],
3804 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3805
3806 async fn aggregate_grouping(
3808 &self,
3809 grouping: GroupingMode<'_>,
3810 aggregate_exprs: &[&krishiv_plan::expression::Expr],
3811 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3812
3813 async fn pivot(
3815 &self,
3816 group_exprs: &[&krishiv_plan::expression::Expr],
3817 pivot_column: &krishiv_plan::expression::Expr,
3818 aggregate_expr: &krishiv_plan::expression::Expr,
3819 values: &[(krishiv_plan::expression::ScalarValue, String)],
3820 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3821
3822 async fn unpivot(
3824 &self,
3825 columns: &[&str],
3826 name_column: &str,
3827 value_column: &str,
3828 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3829
3830 async fn filter(&self, predicate: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3832
3833 async fn filter_expr(
3835 &self,
3836 predicate: &krishiv_plan::expression::Expr,
3837 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3838
3839 async fn limit(&self, n: usize) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3841
3842 async fn distinct(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3844
3845 async fn drop_nulls(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3847
3848 async fn sample(&self, fraction: f64) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3850
3851 async fn sort(
3853 &self,
3854 columns: &[&str],
3855 descending: &[bool],
3856 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3857
3858 async fn alias(&self, alias: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3860
3861 async fn drop_columns(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3863
3864 async fn rename_column(&self, old: &str, new: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3866
3867 async fn with_column(&self, name: &str, expr: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3869
3870 fn as_any(&self) -> &dyn std::any::Any;
3872
3873 async fn describe(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3875
3876 async fn fill_null(&self, column: &str, value: &str)
3878 -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3879
3880 async fn join(
3882 &self,
3883 right: &dyn KrishivDataFrameOps,
3884 how: &str,
3885 left_on: &[&str],
3886 right_on: &[&str],
3887 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3888
3889 async fn union(
3891 &self,
3892 right: &dyn KrishivDataFrameOps,
3893 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3894
3895 async fn union_distinct(
3896 &self,
3897 right: &dyn KrishivDataFrameOps,
3898 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3899
3900 async fn intersect(
3901 &self,
3902 right: &dyn KrishivDataFrameOps,
3903 distinct: bool,
3904 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3905
3906 async fn except(
3907 &self,
3908 right: &dyn KrishivDataFrameOps,
3909 distinct: bool,
3910 ) -> SqlResult<Box<dyn KrishivDataFrameOps>>;
3911
3912 async fn register_batches(&self, name: &str, batches: Vec<RecordBatch>) -> SqlResult<()>;
3915
3916 async fn deregister_table(&self, name: &str) -> SqlResult<()>;
3918
3919 async fn create_view(&self, name: &str, replace: bool) -> SqlResult<()>;
3922}
3923
3924fn df_plan_to_krishiv_nodes(
3932 plan: &datafusion::logical_expr::LogicalPlan,
3933 table_row_counts: &std::collections::HashMap<String, u64>,
3934 counter: &mut usize,
3935) -> (Vec<krishiv_plan::PlanNode>, String) {
3936 use datafusion::logical_expr::LogicalPlan as DfPlan;
3937 use krishiv_plan::{ExecutionKind, NodeOp, PlanNode};
3938
3939 *counter += 1;
3940 let idx = *counter;
3941
3942 match plan {
3943 DfPlan::TableScan(ts) => {
3944 let table_name = ts.table_name.table().to_string();
3945 let row_count = table_row_counts.get(&table_name).copied();
3946 let filters: Vec<String> = ts.filters.iter().map(|e| e.to_string()).collect();
3947 let id = format!("scan-{idx}");
3948 let node = PlanNode::new(&id, format!("Scan {table_name}"), ExecutionKind::Batch)
3949 .with_op(NodeOp::Scan {
3950 table: table_name,
3951 filters,
3952 })
3953 .with_estimated_rows(row_count);
3954 (vec![node], id)
3955 }
3956
3957 DfPlan::Projection(proj) => {
3958 let (mut nodes, input_id) =
3959 df_plan_to_krishiv_nodes(&proj.input, table_row_counts, counter);
3960 let id = format!("proj-{idx}");
3961 let columns: Vec<String> = proj.expr.iter().map(|e| e.to_string()).collect();
3962 nodes.push(
3963 PlanNode::new(&id, "Projection", ExecutionKind::Batch)
3964 .with_op(NodeOp::Project { columns })
3965 .with_inputs([input_id]),
3966 );
3967 (nodes, id)
3968 }
3969
3970 DfPlan::Filter(filter) => {
3971 let (mut nodes, input_id) =
3972 df_plan_to_krishiv_nodes(&filter.input, table_row_counts, counter);
3973 let id = format!("filter-{idx}");
3974 let predicate = filter.predicate.to_string();
3975 nodes.push(
3976 PlanNode::new(&id, "Filter", ExecutionKind::Batch)
3977 .with_op(NodeOp::Filter { predicate })
3978 .with_inputs([input_id]),
3979 );
3980 (nodes, id)
3981 }
3982
3983 DfPlan::Aggregate(agg) => {
3984 let (mut nodes, input_id) =
3985 df_plan_to_krishiv_nodes(&agg.input, table_row_counts, counter);
3986 let id = format!("agg-{idx}");
3987 let group_keys: Vec<String> = agg.group_expr.iter().map(|e| e.to_string()).collect();
3988 nodes.push(
3989 PlanNode::new(&id, "Aggregate", ExecutionKind::Batch)
3990 .with_op(NodeOp::Aggregate { group_keys })
3991 .with_inputs([input_id]),
3992 );
3993 (nodes, id)
3994 }
3995
3996 DfPlan::Join(join) => {
3997 let (mut nodes, left_id) =
3998 df_plan_to_krishiv_nodes(&join.left, table_row_counts, counter);
3999 let (right_nodes, right_id) =
4000 df_plan_to_krishiv_nodes(&join.right, table_row_counts, counter);
4001 nodes.extend(right_nodes);
4002 let id = format!("join-{idx}");
4003 let krishiv_join_type = match join.join_type {
4008 datafusion::common::JoinType::Inner => krishiv_plan::JoinType::Inner,
4009 datafusion::common::JoinType::Left => krishiv_plan::JoinType::Left,
4010 datafusion::common::JoinType::Right => krishiv_plan::JoinType::Right,
4011 datafusion::common::JoinType::Full => krishiv_plan::JoinType::Full,
4012 datafusion::common::JoinType::LeftSemi => krishiv_plan::JoinType::LeftSemi,
4013 datafusion::common::JoinType::RightSemi => krishiv_plan::JoinType::RightSemi,
4014 datafusion::common::JoinType::LeftAnti => krishiv_plan::JoinType::LeftAnti,
4015 datafusion::common::JoinType::RightAnti => krishiv_plan::JoinType::RightAnti,
4016 datafusion::common::JoinType::LeftMark => krishiv_plan::JoinType::LeftSemi,
4020 datafusion::common::JoinType::RightMark => krishiv_plan::JoinType::RightSemi,
4021 };
4022 nodes.push(
4023 PlanNode::new(&id, "Join", ExecutionKind::Batch)
4024 .with_op(NodeOp::Join {
4025 join_type: krishiv_join_type,
4026 })
4027 .with_inputs([left_id, right_id]),
4028 );
4029 (nodes, id)
4030 }
4031
4032 DfPlan::Sort(sort) => {
4033 let (mut nodes, input_id) =
4034 df_plan_to_krishiv_nodes(&sort.input, table_row_counts, counter);
4035 let id = format!("sort-{idx}");
4036 nodes.push(
4037 PlanNode::new(&id, "Sort", ExecutionKind::Batch)
4038 .with_op(NodeOp::Other {
4039 description: format!(
4040 "Sort({})",
4041 sort.expr
4042 .iter()
4043 .map(|e| e.to_string())
4044 .collect::<Vec<_>>()
4045 .join(", ")
4046 ),
4047 })
4048 .with_inputs([input_id]),
4049 );
4050 (nodes, id)
4051 }
4052
4053 DfPlan::Repartition(repart) => {
4054 let (mut nodes, input_id) =
4055 df_plan_to_krishiv_nodes(&repart.input, table_row_counts, counter);
4056 let id = format!("exchange-{idx}");
4057 let partitioning = krishiv_plan::Partitioning::Unpartitioned;
4058 nodes.push(
4059 PlanNode::new(&id, "Exchange", ExecutionKind::Batch)
4060 .with_op(NodeOp::Exchange { partitioning })
4061 .with_inputs([input_id]),
4062 );
4063 (nodes, id)
4064 }
4065
4066 DfPlan::Limit(limit) => {
4067 let (mut nodes, input_id) =
4068 df_plan_to_krishiv_nodes(&limit.input, table_row_counts, counter);
4069 let id = format!("limit-{idx}");
4070 nodes.push(
4071 PlanNode::new(&id, "Limit", ExecutionKind::Batch)
4072 .with_op(NodeOp::Other {
4073 description: format!(
4074 "Limit(skip={:?}, fetch={:?})",
4075 limit.skip.as_ref().map(|e| e.to_string()),
4076 limit.fetch.as_ref().map(|e| e.to_string()),
4077 ),
4078 })
4079 .with_inputs([input_id]),
4080 );
4081 (nodes, id)
4082 }
4083
4084 DfPlan::Union(union) if union.inputs.len() == 1 => {
4085 if let Some(input) = union.inputs.first() {
4086 df_plan_to_krishiv_nodes(input, table_row_counts, counter)
4087 } else {
4088 (Vec::new(), String::new())
4089 }
4090 }
4091 DfPlan::Union(union) => {
4092 let mut all_nodes = Vec::new();
4093 let mut input_ids = Vec::new();
4094 for input in &union.inputs {
4095 let (sub_nodes, sub_id) =
4096 df_plan_to_krishiv_nodes(input, table_row_counts, counter);
4097 all_nodes.extend(sub_nodes);
4098 input_ids.push(sub_id);
4099 }
4100 let id = format!("union-{idx}");
4101 all_nodes.push(
4102 PlanNode::new(&id, "Union", ExecutionKind::Batch)
4103 .with_op(NodeOp::Other {
4104 description: "Union".to_string(),
4105 })
4106 .with_inputs(input_ids),
4107 );
4108 (all_nodes, id)
4109 }
4110
4111 DfPlan::SubqueryAlias(alias) => {
4112 df_plan_to_krishiv_nodes(&alias.input, table_row_counts, counter)
4114 }
4115
4116 DfPlan::Values(_) => {
4117 let id = format!("values-{idx}");
4118 let node = PlanNode::new(&id, "Values", ExecutionKind::Batch).with_op(NodeOp::Other {
4119 description: "Values".to_string(),
4120 });
4121 (vec![node], id)
4122 }
4123
4124 DfPlan::Extension(_) => {
4125 let id = format!("ext-{idx}");
4126 let label = plan.to_string();
4127 let node = PlanNode::new(&id, label.clone(), ExecutionKind::Batch)
4128 .with_op(NodeOp::Other { description: label });
4129 (vec![node], id)
4130 }
4131
4132 DfPlan::EmptyRelation(_) => {
4133 let id = format!("empty-{idx}");
4134 let node =
4135 PlanNode::new(&id, "EmptyRelation", ExecutionKind::Batch).with_op(NodeOp::Other {
4136 description: "EmptyRelation".to_string(),
4137 });
4138 (vec![node], id)
4139 }
4140
4141 _ => {
4143 let id = format!("df-{idx}");
4144 let label = plan.to_string();
4145 let node = PlanNode::new(&id, label.clone(), ExecutionKind::Batch)
4146 .with_op(NodeOp::Other { description: label });
4147 (vec![node], id)
4148 }
4149 }
4150}
4151
4152#[derive(Clone)]
4154pub struct SqlDataFrame {
4155 name: String,
4156 query: Option<String>,
4157 query_text: Option<String>,
4159 execution_kind: ExecutionKind,
4160 dataframe: DataFusionDataFrame,
4161 shuffle_partitions: Option<u32>,
4162 context: SessionContext,
4164 table_row_counts: Arc<std::sync::RwLock<HashMap<String, u64>>>,
4168}
4169
4170impl fmt::Debug for SqlDataFrame {
4171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4172 f.debug_struct("SqlDataFrame")
4173 .field("name", &self.name)
4174 .field("query", &self.query)
4175 .field("shuffle_partitions", &self.shuffle_partitions)
4176 .finish_non_exhaustive()
4177 }
4178}
4179
4180impl SqlDataFrame {
4181 fn new(
4182 name: impl Into<String>,
4183 dataframe: DataFusionDataFrame,
4184 table_row_counts: Arc<std::sync::RwLock<HashMap<String, u64>>>,
4185 ) -> Self {
4186 Self {
4187 name: name.into(),
4188 query: None,
4189 query_text: None,
4190 execution_kind: ExecutionKind::Batch,
4191 dataframe,
4192 shuffle_partitions: None,
4193 context: SessionContext::default(),
4194 table_row_counts,
4195 }
4196 }
4197
4198 pub(crate) fn with_context(mut self, context: SessionContext) -> Self {
4200 self.context = context;
4201 self
4202 }
4203
4204 fn with_query(mut self, query: impl Into<String>) -> Self {
4205 let q = query.into();
4206 self.query_text = Some(q.clone());
4207 self.query = Some(q);
4208 self
4209 }
4210
4211 fn with_execution_kind(mut self, kind: ExecutionKind) -> Self {
4212 self.execution_kind = kind;
4213 self
4214 }
4215
4216 fn with_shuffle_partitions(mut self, n: Option<u32>) -> Self {
4217 self.shuffle_partitions = n;
4218 self
4219 }
4220
4221 pub fn query(&self) -> Option<&str> {
4223 self.query.as_deref()
4224 }
4225
4226 pub fn arrow_schema(&self) -> arrow::datatypes::SchemaRef {
4232 std::sync::Arc::new(self.dataframe.schema().as_arrow().clone())
4233 }
4234
4235 fn with_new_dataframe(&self, df: DataFusionDataFrame, tag: &str) -> Self {
4239 Self {
4240 name: format!("{}-{}", self.name, tag),
4241 query: None,
4242 query_text: None,
4243 execution_kind: self.execution_kind,
4244 dataframe: df,
4245 shuffle_partitions: self.shuffle_partitions,
4246 context: self.context.clone(),
4247 table_row_counts: self.table_row_counts.clone(),
4248 }
4249 }
4250
4251 pub fn krishiv_logical_plan(&self) -> LogicalPlan {
4260 let df_plan = self.dataframe.logical_plan();
4261 let counts = self
4262 .table_row_counts
4263 .read()
4264 .unwrap_or_else(|e| e.into_inner());
4265 let mut counter = 0usize;
4266 let (nodes, _root_id) = df_plan_to_krishiv_nodes(df_plan, &counts, &mut counter);
4267
4268 let mut plan = LogicalPlan::new(self.name.clone(), self.execution_kind);
4269 for node in nodes {
4270 plan = plan.with_node(node);
4271 }
4272
4273 let optimizer = krishiv_plan::optimizer::default_logical_optimizer();
4278 let fallback = plan.clone();
4279 match optimizer.optimize(plan) {
4280 Ok(result) => result.plan,
4281 Err(error) => {
4282 tracing::warn!(
4283 plan = %self.name,
4284 %error,
4285 "logical optimizer failed; using unoptimized plan"
4286 );
4287 fallback
4288 }
4289 }
4290 }
4291
4292 pub fn explain_logical(&self) -> String {
4294 self.dataframe.logical_plan().to_string()
4295 }
4296
4297 pub async fn explain(&self) -> SqlResult<String> {
4299 let batches = self
4300 .dataframe
4301 .clone()
4302 .explain(false, false)?
4303 .collect()
4304 .await?;
4305 pretty_batches(&batches)
4306 }
4307
4308 pub async fn explain_analyze(&self) -> SqlResult<String> {
4323 let batches = self
4324 .dataframe
4325 .clone()
4326 .explain(false, true)?
4327 .collect()
4328 .await?;
4329 pretty_batches(&batches)
4330 }
4331
4332 pub fn collect(
4337 &self,
4338 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = SqlResult<Vec<RecordBatch>>> + Send + '_>>
4339 {
4340 Box::pin(async move { Ok(self.dataframe.clone().collect().await?) })
4341 }
4342
4343 pub fn execute_stream(
4349 &self,
4350 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = SqlResult<SqlStream>> + Send + '_>>
4351 {
4352 Box::pin(self.execute_stream_boxed_body())
4353 }
4354
4355 async fn execute_stream_boxed_body(&self) -> SqlResult<SqlStream> {
4356 Ok(self.execute_stream_with_schema_boxed_body().await?.1)
4357 }
4358
4359 pub fn execute_stream_with_schema(
4374 &self,
4375 ) -> futures::future::BoxFuture<'_, SqlResult<(SchemaRef, SqlStream)>> {
4376 Box::pin(self.execute_stream_with_schema_boxed_body())
4377 }
4378
4379 async fn execute_stream_with_schema_boxed_body(&self) -> SqlResult<(SchemaRef, SqlStream)> {
4380 let df_stream = self.dataframe.clone().execute_stream().await?;
4381 let schema = df_stream.schema();
4382 use futures::StreamExt;
4383 let mapped = df_stream.map(|res| {
4384 res.map_err(|e| SqlError::DataFusion {
4385 message: e.to_string(),
4386 })
4387 });
4388 Ok((schema, Box::pin(mapped)))
4389 }
4390
4391 pub fn collect_with_stats(
4399 &self,
4400 ) -> futures::future::BoxFuture<'_, SqlResult<(Vec<RecordBatch>, SqlExecutionStats)>> {
4401 Box::pin(self.collect_with_stats_boxed_body())
4402 }
4403
4404 async fn collect_with_stats_boxed_body(
4405 &self,
4406 ) -> SqlResult<(Vec<RecordBatch>, SqlExecutionStats)> {
4407 use datafusion::physical_plan::collect as df_collect;
4408
4409 let df = self.dataframe.clone();
4410 let task_ctx = df.task_ctx();
4411 let physical_plan = df.create_physical_plan().await?;
4412
4413 let batches = df_collect(physical_plan.clone(), task_ctx.into()).await?;
4414
4415 let mut output_rows: u64 = batches.iter().map(|b| b.num_rows() as u64).sum();
4416 let mut cpu_nanos: u64 = 0;
4417
4418 if let Some(metrics) = physical_plan.metrics() {
4419 if let Some(v) = metrics.output_rows() {
4420 output_rows = v as u64;
4421 }
4422 if let Some(t) = metrics.elapsed_compute() {
4423 cpu_nanos = t as u64;
4424 }
4425 }
4426
4427 let (spill_bytes, spill_count) = aggregate_spill_metrics(physical_plan.as_ref());
4428
4429 Ok((
4430 batches,
4431 SqlExecutionStats {
4432 output_rows,
4433 cpu_nanos,
4434 spill_bytes,
4435 spill_count,
4436 },
4437 ))
4438 }
4439
4440 pub fn execute_stream_with_stats(
4450 &self,
4451 ) -> futures::future::BoxFuture<'_, SqlResult<(SqlStream, SqlStatsHandle)>> {
4452 Box::pin(self.execute_stream_with_stats_boxed_body())
4453 }
4454
4455 async fn execute_stream_with_stats_boxed_body(&self) -> SqlResult<(SqlStream, SqlStatsHandle)> {
4456 use futures::StreamExt;
4457
4458 let df = self.dataframe.clone();
4459 let task_ctx = df.task_ctx();
4460 let physical_plan = df.create_physical_plan().await?;
4461 let df_stream = datafusion::physical_plan::execute_stream(
4462 physical_plan.clone(),
4463 std::sync::Arc::new(task_ctx),
4464 )?;
4465 let mapped = df_stream.map(|res| {
4466 res.map_err(|e| SqlError::DataFusion {
4467 message: e.to_string(),
4468 })
4469 });
4470 Ok((
4471 Box::pin(mapped),
4472 SqlStatsHandle {
4473 plan: physical_plan,
4474 },
4475 ))
4476 }
4477}
4478
4479pub struct SqlStatsHandle {
4482 plan: std::sync::Arc<dyn datafusion::physical_plan::ExecutionPlan>,
4483}
4484
4485impl SqlStatsHandle {
4486 pub fn stats(&self) -> SqlExecutionStats {
4491 let mut output_rows: u64 = 0;
4492 let mut cpu_nanos: u64 = 0;
4493 if let Some(metrics) = self.plan.metrics() {
4494 if let Some(v) = metrics.output_rows() {
4495 output_rows = v as u64;
4496 }
4497 if let Some(t) = metrics.elapsed_compute() {
4498 cpu_nanos = t as u64;
4499 }
4500 }
4501 let (spill_bytes, spill_count) = aggregate_spill_metrics(self.plan.as_ref());
4502 SqlExecutionStats {
4503 output_rows,
4504 cpu_nanos,
4505 spill_bytes,
4506 spill_count,
4507 }
4508 }
4509}
4510
4511fn aggregate_spill_metrics(plan: &dyn datafusion::physical_plan::ExecutionPlan) -> (u64, u64) {
4518 let mut spill_bytes: u64 = 0;
4519 let mut spill_count: u64 = 0;
4520 if let Some(metrics) = plan.metrics() {
4521 if let Some(bytes) = metrics.spilled_bytes() {
4522 spill_bytes = spill_bytes.saturating_add(bytes as u64);
4523 }
4524 if let Some(count) = metrics.spill_count() {
4525 spill_count = spill_count.saturating_add(count as u64);
4526 }
4527 }
4528 for child in plan.children() {
4529 let (child_bytes, child_count) = aggregate_spill_metrics(child.as_ref());
4530 spill_bytes = spill_bytes.saturating_add(child_bytes);
4531 spill_count = spill_count.saturating_add(child_count);
4532 }
4533 (spill_bytes, spill_count)
4534}
4535
4536#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4538pub struct SqlExecutionStats {
4539 pub output_rows: u64,
4540 pub cpu_nanos: u64,
4541 pub spill_bytes: u64,
4543 pub spill_count: u64,
4545}
4546
4547fn top_level_alias_index(expression: &str) -> Option<usize> {
4548 let bytes = expression.as_bytes();
4549 let mut depth = 0usize;
4550 let mut single_quoted = false;
4551 let mut double_quoted = false;
4552 let mut candidate = None;
4553 let mut index = 0usize;
4554 while index < bytes.len() {
4555 let Some(&byte) = bytes.get(index) else {
4556 break;
4557 };
4558 match byte {
4559 b'\'' if !double_quoted => {
4560 if single_quoted && bytes.get(index + 1) == Some(&b'\'') {
4561 index += 2;
4562 continue;
4563 }
4564 single_quoted = !single_quoted;
4565 }
4566 b'"' if !single_quoted => {
4567 if double_quoted && bytes.get(index + 1) == Some(&b'"') {
4568 index += 2;
4569 continue;
4570 }
4571 double_quoted = !double_quoted;
4572 }
4573 b'(' if !single_quoted && !double_quoted => depth += 1,
4574 b')' if !single_quoted && !double_quoted => depth = depth.saturating_sub(1),
4575 b' ' if depth == 0
4576 && !single_quoted
4577 && !double_quoted
4578 && bytes
4579 .get(index..index + 4)
4580 .is_some_and(|slice| slice.eq_ignore_ascii_case(b" AS ")) =>
4581 {
4582 candidate = Some(index);
4583 index += 3;
4584 }
4585 _ => {}
4586 }
4587 index += 1;
4588 }
4589 candidate
4590}
4591
4592fn parse_dataframe_expression(
4593 dataframe: &datafusion::dataframe::DataFrame,
4594 expression: &str,
4595) -> SqlResult<datafusion::logical_expr::Expr> {
4596 if let Some(index) = top_level_alias_index(expression) {
4597 let (body, alias) = expression.split_at(index);
4598 let alias = alias[4..].trim();
4599 if !alias.is_empty() {
4600 let alias = alias
4601 .strip_prefix('"')
4602 .and_then(|value| value.strip_suffix('"'))
4603 .unwrap_or(alias)
4604 .replace("\"\"", "\"");
4605 return Ok(dataframe.parse_sql_expr(body.trim())?.alias(alias));
4606 }
4607 }
4608 dataframe.parse_sql_expr(expression).map_err(Into::into)
4609}
4610
4611pub fn parse_public_expression(sql: &str) -> SqlResult<krishiv_plan::expression::Expr> {
4613 let dialect = GenericDialect {};
4614 let mut parser =
4615 Parser::new(&dialect)
4616 .try_with_sql(sql)
4617 .map_err(|error| SqlError::Unsupported {
4618 feature: format!("public expression parse: {error}"),
4619 })?;
4620 let expression = parser.parse_expr().map_err(|error| SqlError::Unsupported {
4621 feature: format!("public expression parse: {error}"),
4622 })?;
4623 sqlparser_expression_to_public(&expression)
4624}
4625
4626fn sqlparser_expression_to_public(
4627 expression: &datafusion::sql::sqlparser::ast::Expr,
4628) -> SqlResult<krishiv_plan::expression::Expr> {
4629 use datafusion::sql::sqlparser::ast::{BinaryOperator as SqlOperator, Expr as SqlExpr, Value};
4630 use krishiv_plan::expression::{BinaryOperator, Expr, ScalarValue};
4631
4632 Ok(match expression {
4633 SqlExpr::Identifier(identifier) => Expr::Column {
4634 path: vec![identifier.value.clone()],
4635 },
4636 SqlExpr::CompoundIdentifier(identifiers) => Expr::Column {
4637 path: identifiers
4638 .iter()
4639 .map(|identifier| identifier.value.clone())
4640 .collect(),
4641 },
4642 SqlExpr::Nested(expression) => sqlparser_expression_to_public(expression)?,
4643 SqlExpr::IsNull(expression) => Expr::IsNull {
4644 expression: Box::new(sqlparser_expression_to_public(expression)?),
4645 negated: false,
4646 },
4647 SqlExpr::IsNotNull(expression) => Expr::IsNull {
4648 expression: Box::new(sqlparser_expression_to_public(expression)?),
4649 negated: true,
4650 },
4651 SqlExpr::BinaryOp { left, op, right } => Expr::Binary {
4652 left: Box::new(sqlparser_expression_to_public(left)?),
4653 op: match op {
4654 SqlOperator::Eq => BinaryOperator::Eq,
4655 SqlOperator::NotEq => BinaryOperator::NotEq,
4656 SqlOperator::Gt => BinaryOperator::Gt,
4657 SqlOperator::GtEq => BinaryOperator::GtEq,
4658 SqlOperator::Lt => BinaryOperator::Lt,
4659 SqlOperator::LtEq => BinaryOperator::LtEq,
4660 SqlOperator::And => BinaryOperator::And,
4661 SqlOperator::Or => BinaryOperator::Or,
4662 SqlOperator::Plus => BinaryOperator::Plus,
4663 SqlOperator::Minus => BinaryOperator::Minus,
4664 SqlOperator::Multiply => BinaryOperator::Multiply,
4665 SqlOperator::Divide => BinaryOperator::Divide,
4666 other => {
4667 return Err(SqlError::Unsupported {
4668 feature: format!("public expression operator {other}"),
4669 });
4670 }
4671 },
4672 right: Box::new(sqlparser_expression_to_public(right)?),
4673 },
4674 SqlExpr::Value(value) => Expr::Literal {
4675 value: match &value.value {
4676 Value::Null => ScalarValue::Null,
4677 Value::Boolean(value) => ScalarValue::Boolean(*value),
4678 Value::SingleQuotedString(value) => ScalarValue::Utf8(value.clone()),
4679 Value::Number(value, _)
4680 if value.contains('.') || value.contains('e') || value.contains('E') =>
4681 {
4682 ScalarValue::float64(value.parse::<f64>().map_err(|error| {
4683 SqlError::Unsupported {
4684 feature: format!("numeric expression literal: {error}"),
4685 }
4686 })?)
4687 }
4688 Value::Number(value, _) => {
4689 ScalarValue::Int64(value.parse::<i64>().map_err(|error| {
4690 SqlError::Unsupported {
4691 feature: format!("integer expression literal: {error}"),
4692 }
4693 })?)
4694 }
4695 other => {
4696 return Err(SqlError::Unsupported {
4697 feature: format!("public expression literal {other}"),
4698 });
4699 }
4700 },
4701 },
4702 other => {
4703 return Err(SqlError::Unsupported {
4704 feature: format!("public expression node {other}"),
4705 });
4706 }
4707 })
4708}
4709
4710fn public_data_type_to_arrow(
4711 data_type: &krishiv_plan::expression::ExprDataType,
4712) -> arrow::datatypes::DataType {
4713 use arrow::datatypes::{DataType, Field, IntervalUnit, TimeUnit};
4714 use krishiv_plan::expression::{ExprDataType, IntervalUnit as PublicIntervalUnit};
4715
4716 match data_type {
4717 ExprDataType::Null => DataType::Null,
4718 ExprDataType::Boolean => DataType::Boolean,
4719 ExprDataType::Int64 => DataType::Int64,
4720 ExprDataType::UInt64 => DataType::UInt64,
4721 ExprDataType::Float64 => DataType::Float64,
4722 ExprDataType::Utf8 => DataType::Utf8,
4723 ExprDataType::Binary => DataType::Binary,
4724 ExprDataType::Decimal128 { precision, scale } => DataType::Decimal128(*precision, *scale),
4725 ExprDataType::Date32 => DataType::Date32,
4726 ExprDataType::Timestamp { unit, timezone } => DataType::Timestamp(
4727 match unit {
4728 krishiv_plan::expression::TimeUnit::Second => TimeUnit::Second,
4729 krishiv_plan::expression::TimeUnit::Millisecond => TimeUnit::Millisecond,
4730 krishiv_plan::expression::TimeUnit::Microsecond => TimeUnit::Microsecond,
4731 krishiv_plan::expression::TimeUnit::Nanosecond => TimeUnit::Nanosecond,
4732 },
4733 timezone.clone().map(Into::into),
4734 ),
4735 ExprDataType::Interval { unit } => DataType::Interval(match unit {
4736 PublicIntervalUnit::YearMonth => IntervalUnit::YearMonth,
4737 PublicIntervalUnit::DayTime => IntervalUnit::DayTime,
4738 PublicIntervalUnit::MonthDayNano => IntervalUnit::MonthDayNano,
4739 }),
4740 ExprDataType::List(element) => DataType::List(Arc::new(Field::new(
4741 "item",
4742 public_data_type_to_arrow(element),
4743 true,
4744 ))),
4745 ExprDataType::Map { key, value } => DataType::Map(
4746 Arc::new(Field::new(
4747 "entries",
4748 DataType::Struct(
4749 vec![
4750 Arc::new(Field::new("key", public_data_type_to_arrow(key), false)),
4751 Arc::new(Field::new("value", public_data_type_to_arrow(value), true)),
4752 ]
4753 .into(),
4754 ),
4755 false,
4756 )),
4757 false,
4758 ),
4759 ExprDataType::Struct(fields) => DataType::Struct(
4760 fields
4761 .iter()
4762 .map(|field| {
4763 Arc::new(Field::new(
4764 &field.name,
4765 public_data_type_to_arrow(&field.data_type),
4766 field.nullable,
4767 ))
4768 })
4769 .collect::<Vec<_>>()
4770 .into(),
4771 ),
4772 ExprDataType::Variant => DataType::Utf8,
4777 }
4778}
4779
4780fn public_scalar_to_datafusion(
4781 value: &krishiv_plan::expression::ScalarValue,
4782) -> Option<datafusion::common::ScalarValue> {
4783 use datafusion::common::ScalarValue;
4784 use krishiv_plan::expression::{ScalarValue as PublicScalar, TimeUnit};
4785
4786 Some(match value {
4787 PublicScalar::Null => ScalarValue::Null,
4788 PublicScalar::Boolean(value) => ScalarValue::Boolean(Some(*value)),
4789 PublicScalar::Int64(value) => ScalarValue::Int64(Some(*value)),
4790 PublicScalar::UInt64(value) => ScalarValue::UInt64(Some(*value)),
4791 PublicScalar::Float64(bits) => ScalarValue::Float64(Some(f64::from_bits(*bits))),
4792 PublicScalar::Utf8(value) => ScalarValue::Utf8(Some(value.clone())),
4793 PublicScalar::Binary(value) => ScalarValue::Binary(Some(value.clone())),
4794 PublicScalar::Decimal128 {
4795 value,
4796 precision,
4797 scale,
4798 } => ScalarValue::Decimal128(Some(*value), *precision, *scale),
4799 PublicScalar::Date32(value) => ScalarValue::Date32(Some(*value)),
4800 PublicScalar::Timestamp {
4801 value,
4802 unit,
4803 timezone,
4804 } => {
4805 let timezone = timezone.clone().map(Into::into);
4806 match unit {
4807 TimeUnit::Second => ScalarValue::TimestampSecond(Some(*value), timezone),
4808 TimeUnit::Millisecond => ScalarValue::TimestampMillisecond(Some(*value), timezone),
4809 TimeUnit::Microsecond => ScalarValue::TimestampMicrosecond(Some(*value), timezone),
4810 TimeUnit::Nanosecond => ScalarValue::TimestampNanosecond(Some(*value), timezone),
4811 }
4812 }
4813 PublicScalar::Interval { .. } => return None,
4814 })
4815}
4816
4817fn lower_public_expression(
4823 dataframe: &datafusion::dataframe::DataFrame,
4824 expression: &krishiv_plan::expression::Expr,
4825) -> SqlResult<datafusion::logical_expr::Expr> {
4826 expression
4827 .validate()
4828 .map_err(|error| SqlError::Unsupported {
4829 feature: format!("invalid public expression: {error}"),
4830 })?;
4831 use datafusion::logical_expr::{Expr as DataFusionExpr, Operator, binary_expr, cast, try_cast};
4832 use krishiv_plan::expression::{BinaryOperator, Expr};
4833
4834 Ok(match expression {
4835 Expr::Column { path } if path.len() == 1 => {
4836 datafusion::prelude::col(path.first().map(String::as_str).unwrap_or(""))
4837 }
4838 Expr::Column { .. } => parse_dataframe_expression(dataframe, &expression.to_sql())?,
4839 Expr::Literal { value } => match public_scalar_to_datafusion(value) {
4840 Some(value) => DataFusionExpr::Literal(value, None),
4841 None => parse_dataframe_expression(dataframe, &expression.to_sql())?,
4842 },
4843 Expr::Alias { expression, name } => {
4844 lower_public_expression(dataframe, expression)?.alias(name)
4845 }
4846 Expr::Binary { left, op, right } => binary_expr(
4847 lower_public_expression(dataframe, left)?,
4848 match op {
4849 BinaryOperator::Eq => Operator::Eq,
4850 BinaryOperator::NotEq => Operator::NotEq,
4851 BinaryOperator::Gt => Operator::Gt,
4852 BinaryOperator::GtEq => Operator::GtEq,
4853 BinaryOperator::Lt => Operator::Lt,
4854 BinaryOperator::LtEq => Operator::LtEq,
4855 BinaryOperator::And => Operator::And,
4856 BinaryOperator::Or => Operator::Or,
4857 BinaryOperator::Plus => Operator::Plus,
4858 BinaryOperator::Minus => Operator::Minus,
4859 BinaryOperator::Multiply => Operator::Multiply,
4860 BinaryOperator::Divide => Operator::Divide,
4861 },
4862 lower_public_expression(dataframe, right)?,
4863 ),
4864 Expr::IsNull {
4865 expression,
4866 negated,
4867 } => {
4868 let expression = lower_public_expression(dataframe, expression)?;
4869 if *negated {
4870 expression.is_not_null()
4871 } else {
4872 expression.is_null()
4873 }
4874 }
4875 Expr::Cast {
4876 expression,
4877 data_type,
4878 safe,
4879 } => {
4880 let expression = lower_public_expression(dataframe, expression)?;
4881 let data_type = public_data_type_to_arrow(data_type);
4882 if *safe {
4883 try_cast(expression, data_type)
4884 } else {
4885 cast(expression, data_type)
4886 }
4887 }
4888 Expr::Sort { .. } => {
4889 return Err(SqlError::Unsupported {
4890 feature: "standalone sort expressions are only valid inside windows or order_by"
4891 .into(),
4892 });
4893 }
4894 Expr::Aggregate { .. }
4895 | Expr::Function { .. }
4896 | Expr::Window { .. }
4897 | Expr::RawSql { .. } => parse_dataframe_expression(dataframe, &expression.to_sql())?,
4898 })
4899}
4900
4901fn sql_dataframe<'a>(
4902 dataframe: &'a dyn KrishivDataFrameOps,
4903 operation: &str,
4904) -> SqlResult<&'a SqlDataFrame> {
4905 dataframe
4906 .as_any()
4907 .downcast_ref::<SqlDataFrame>()
4908 .ok_or_else(|| SqlError::DataFusion {
4909 message: format!("right DataFrame must be SqlDataFrame for {operation}"),
4910 })
4911}
4912
4913#[async_trait::async_trait]
4914impl KrishivDataFrameOps for SqlDataFrame {
4915 async fn collect(&self) -> SqlResult<Vec<RecordBatch>> {
4916 SqlDataFrame::collect(self).await
4917 }
4918 async fn collect_with_stats(&self) -> SqlResult<(Vec<RecordBatch>, SqlExecutionStats)> {
4919 SqlDataFrame::collect_with_stats(self).await
4920 }
4921 async fn explain_analyze(&self) -> SqlResult<String> {
4922 SqlDataFrame::explain_analyze(self).await
4923 }
4924
4925 async fn explain(&self) -> SqlResult<String> {
4926 SqlDataFrame::explain(self).await
4927 }
4928 fn explain_logical(&self) -> String {
4929 SqlDataFrame::explain_logical(self)
4930 }
4931 fn krishiv_logical_plan(&self) -> LogicalPlan {
4932 let label = self.dataframe.logical_plan().to_string();
4933 let mut plan = LogicalPlan::new(self.name.clone(), ExecutionKind::Batch).with_node(
4934 PlanNode::new("datafusion-logical", label, ExecutionKind::Batch),
4935 );
4936 if let Some(n) = self.shuffle_partitions {
4937 plan = plan.with_shuffle_partitions(Some(n));
4938 }
4939 plan
4940 }
4941 fn query(&self) -> Option<&str> {
4942 SqlDataFrame::query(self)
4943 }
4944 fn to_sql(&self) -> SqlResult<String> {
4945 match datafusion::sql::unparser::plan_to_sql(self.dataframe.logical_plan()) {
4948 Ok(statement) => Ok(statement.to_string()),
4949 Err(err) => self
4950 .query()
4951 .map(str::to_string)
4952 .ok_or_else(|| SqlError::Unsupported {
4953 feature: format!("cannot render DataFrame plan as SQL: {err}"),
4954 }),
4955 }
4956 }
4957 async fn execute_stream(&self) -> SqlResult<SqlStream> {
4958 SqlDataFrame::execute_stream(self).await
4959 }
4960
4961 fn schema(&self) -> SchemaRef {
4964 SchemaRef::from(self.dataframe.schema().clone())
4965 }
4966
4967 async fn select(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4968 let df = self.dataframe.clone().select_columns(columns)?;
4969 Ok(Box::new(self.with_new_dataframe(df, "select")))
4970 }
4971
4972 async fn select_exprs(
4973 &self,
4974 expressions: &[&krishiv_plan::expression::Expr],
4975 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4976 let expressions = expressions
4977 .iter()
4978 .map(|expression| lower_public_expression(&self.dataframe, expression))
4979 .collect::<Result<Vec<_>, _>>()?;
4980 let df = self.dataframe.clone().select(expressions)?;
4981 Ok(Box::new(self.with_new_dataframe(df, "select_exprs")))
4982 }
4983
4984 async fn unnest_columns(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4985 let df = self.dataframe.clone().unnest_columns(columns)?;
4986 Ok(Box::new(self.with_new_dataframe(df, "unnest")))
4987 }
4988
4989 async fn aggregate(
4990 &self,
4991 group_exprs: &[&krishiv_plan::expression::Expr],
4992 aggregate_exprs: &[&krishiv_plan::expression::Expr],
4993 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
4994 if aggregate_exprs.is_empty() {
4995 return Err(SqlError::Unsupported {
4996 feature: "aggregate requires at least one aggregate expression".into(),
4997 });
4998 }
4999 let group_exprs = group_exprs
5000 .iter()
5001 .map(|expression| lower_public_expression(&self.dataframe, expression))
5002 .collect::<Result<Vec<_>, _>>()?;
5003 let aggregate_exprs = aggregate_exprs
5004 .iter()
5005 .map(|expression| lower_public_expression(&self.dataframe, expression))
5006 .collect::<Result<Vec<_>, _>>()?;
5007 let df = self
5008 .dataframe
5009 .clone()
5010 .aggregate(group_exprs, aggregate_exprs)?;
5011 Ok(Box::new(self.with_new_dataframe(df, "aggregate")))
5012 }
5013
5014 async fn aggregate_grouping(
5015 &self,
5016 grouping: GroupingMode<'_>,
5017 aggregate_exprs: &[&krishiv_plan::expression::Expr],
5018 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5019 if aggregate_exprs.is_empty() {
5020 return Err(SqlError::Unsupported {
5021 feature: "grouping aggregation requires at least one aggregate expression".into(),
5022 });
5023 }
5024 let lower = |expression: &&krishiv_plan::expression::Expr| {
5025 lower_public_expression(&self.dataframe, expression)
5026 };
5027 let group = match grouping {
5028 GroupingMode::Sets(sets) => datafusion::logical_expr::grouping_set(
5029 sets.into_iter()
5030 .map(|set| set.iter().map(lower).collect::<Result<Vec<_>, _>>())
5031 .collect::<Result<Vec<_>, _>>()?,
5032 ),
5033 GroupingMode::Cube(expressions) => datafusion::logical_expr::cube(
5034 expressions
5035 .iter()
5036 .map(lower)
5037 .collect::<Result<Vec<_>, _>>()?,
5038 ),
5039 GroupingMode::Rollup(expressions) => datafusion::logical_expr::rollup(
5040 expressions
5041 .iter()
5042 .map(lower)
5043 .collect::<Result<Vec<_>, _>>()?,
5044 ),
5045 };
5046 let aggregates = aggregate_exprs
5047 .iter()
5048 .map(lower)
5049 .collect::<Result<Vec<_>, _>>()?;
5050 let df = self.dataframe.clone().aggregate(vec![group], aggregates)?;
5051 Ok(Box::new(self.with_new_dataframe(df, "aggregate_grouping")))
5052 }
5053
5054 async fn pivot(
5055 &self,
5056 group_exprs: &[&krishiv_plan::expression::Expr],
5057 pivot_column: &krishiv_plan::expression::Expr,
5058 aggregate_expr: &krishiv_plan::expression::Expr,
5059 values: &[(krishiv_plan::expression::ScalarValue, String)],
5060 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5061 use krishiv_plan::expression::Expr as PublicExpr;
5062 let (function, input, distinct) = match aggregate_expr {
5063 PublicExpr::Aggregate {
5064 function,
5065 expression: Some(input),
5066 distinct,
5067 } => (*function, input.as_ref(), *distinct),
5068 _ => {
5069 return Err(SqlError::Unsupported {
5070 feature: "pivot requires an aggregate expression with one input".into(),
5071 });
5072 }
5073 };
5074 if values.is_empty() {
5075 return Err(SqlError::Unsupported {
5076 feature: "pivot requires at least one value".into(),
5077 });
5078 }
5079 let group_exprs = group_exprs
5080 .iter()
5081 .map(|expression| lower_public_expression(&self.dataframe, expression))
5082 .collect::<Result<Vec<_>, _>>()?;
5083 let aggregates = values
5084 .iter()
5085 .map(|(value, alias)| {
5086 let conditional = PublicExpr::raw(format!(
5087 "CASE WHEN {} = {} THEN {} END",
5088 pivot_column.to_sql(),
5089 value.to_sql_literal(),
5090 input.to_sql()
5091 ));
5092 let aggregate = PublicExpr::Aggregate {
5093 function,
5094 expression: Some(Box::new(conditional)),
5095 distinct,
5096 }
5097 .alias(alias);
5098 lower_public_expression(&self.dataframe, &aggregate)
5099 })
5100 .collect::<Result<Vec<_>, _>>()?;
5101 let dataframe = self.dataframe.clone().aggregate(group_exprs, aggregates)?;
5102 Ok(Box::new(self.with_new_dataframe(dataframe, "pivot")))
5103 }
5104
5105 async fn unpivot(
5106 &self,
5107 columns: &[&str],
5108 name_column: &str,
5109 value_column: &str,
5110 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5111 if columns.is_empty() {
5112 return Err(SqlError::Unsupported {
5113 feature: "unpivot requires at least one column".into(),
5114 });
5115 }
5116 let retained = self
5117 .dataframe
5118 .schema()
5119 .fields()
5120 .iter()
5121 .map(|field| field.name().as_str())
5122 .filter(|name| !columns.contains(name))
5123 .collect::<Vec<_>>();
5124 let mut branches = Vec::with_capacity(columns.len());
5125 for column in columns {
5126 let mut expressions = retained
5127 .iter()
5128 .map(|name| datafusion::logical_expr::col(*name))
5129 .collect::<Vec<_>>();
5130 expressions
5131 .push(datafusion::logical_expr::lit((*column).to_owned()).alias(name_column));
5132 expressions.push(datafusion::logical_expr::col(*column).alias(value_column));
5133 branches.push(self.dataframe.clone().select(expressions)?);
5134 }
5135 let mut branches = branches.into_iter();
5136 let Some(mut dataframe) = branches.next() else {
5137 return Err(SqlError::Unsupported {
5138 feature: "unpivot requires at least one branch".into(),
5139 });
5140 };
5141 for branch in branches {
5142 dataframe = dataframe.union(branch)?;
5143 }
5144 Ok(Box::new(self.with_new_dataframe(dataframe, "unpivot")))
5145 }
5146
5147 async fn filter(&self, predicate: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5148 let expr = self.dataframe.parse_sql_expr(predicate)?;
5149 let df = self.dataframe.clone().filter(expr)?;
5150 Ok(Box::new(self.with_new_dataframe(df, "filter")))
5151 }
5152
5153 async fn filter_expr(
5154 &self,
5155 predicate: &krishiv_plan::expression::Expr,
5156 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5157 let expr = lower_public_expression(&self.dataframe, predicate)?;
5158 let df = self.dataframe.clone().filter(expr)?;
5159 Ok(Box::new(self.with_new_dataframe(df, "filter_expr")))
5160 }
5161
5162 async fn limit(&self, n: usize) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5163 let df = self.dataframe.clone().limit(0, Some(n))?;
5164 Ok(Box::new(self.with_new_dataframe(df, "limit")))
5165 }
5166
5167 async fn distinct(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5168 let df = self.dataframe.clone().distinct()?;
5169 Ok(Box::new(self.with_new_dataframe(df, "distinct")))
5170 }
5171
5172 async fn drop_nulls(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5173 let columns = if columns.is_empty() {
5174 self.dataframe
5175 .schema()
5176 .fields()
5177 .iter()
5178 .map(|field| field.name().as_str())
5179 .collect::<Vec<_>>()
5180 } else {
5181 columns.to_vec()
5182 };
5183 let mut predicate: Option<datafusion::logical_expr::Expr> = None;
5184 for column in columns {
5185 let next = datafusion::logical_expr::col(column).is_not_null();
5186 predicate = Some(match predicate {
5187 Some(current) => current.and(next),
5188 None => next,
5189 });
5190 }
5191 let df = match predicate {
5192 Some(predicate) => self.dataframe.clone().filter(predicate)?,
5193 None => self.dataframe.clone(),
5194 };
5195 Ok(Box::new(self.with_new_dataframe(df, "drop_nulls")))
5196 }
5197
5198 async fn sample(&self, fraction: f64) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5199 if !(0.0..=1.0).contains(&fraction) {
5200 return Err(SqlError::Unsupported {
5201 feature: "sample fraction must be between 0 and 1".into(),
5202 });
5203 }
5204 let predicate = self
5205 .dataframe
5206 .parse_sql_expr(&format!("random() < {fraction}"))?;
5207 let df = self.dataframe.clone().filter(predicate)?;
5208 Ok(Box::new(self.with_new_dataframe(df, "sample")))
5209 }
5210
5211 async fn sort(
5212 &self,
5213 columns: &[&str],
5214 descending: &[bool],
5215 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5216 use datafusion::logical_expr::SortExpr;
5217 let exprs: Vec<SortExpr> = columns
5218 .iter()
5219 .zip(descending.iter())
5220 .map(|(col_name, desc)| datafusion::logical_expr::col(*col_name).sort(!desc, *desc))
5221 .collect();
5222 let df = self.dataframe.clone().sort(exprs)?;
5223 Ok(Box::new(self.with_new_dataframe(df, "sort")))
5224 }
5225
5226 async fn alias(&self, alias: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5227 let df = self.dataframe.clone().alias(alias)?;
5228 Ok(Box::new(self.with_new_dataframe(df, "alias")))
5229 }
5230
5231 async fn drop_columns(&self, columns: &[&str]) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5232 let df = self.dataframe.clone().drop_columns(columns)?;
5233 Ok(Box::new(self.with_new_dataframe(df, "drop")))
5234 }
5235
5236 async fn rename_column(&self, old: &str, new: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5237 let df = self.dataframe.clone().with_column_renamed(old, new)?;
5238 Ok(Box::new(self.with_new_dataframe(df, "rename")))
5239 }
5240
5241 async fn with_column(&self, name: &str, expr: &str) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5242 let parsed = self.dataframe.parse_sql_expr(expr)?;
5243 let df = self.dataframe.clone().with_column(name, parsed)?;
5244 Ok(Box::new(self.with_new_dataframe(df, "with_column")))
5245 }
5246
5247 fn as_any(&self) -> &dyn std::any::Any {
5248 self
5249 }
5250
5251 async fn describe(&self) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5252 let df = self.dataframe.clone().describe().await?;
5253 Ok(Box::new(self.with_new_dataframe(df, "describe")))
5254 }
5255
5256 async fn fill_null(
5257 &self,
5258 column: &str,
5259 value: &str,
5260 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5261 let expr = format!("COALESCE({column}, {value})");
5262 let parsed = self.dataframe.parse_sql_expr(&expr)?;
5263 let df = self.dataframe.clone().with_column(column, parsed)?;
5264 Ok(Box::new(self.with_new_dataframe(df, "fill_null")))
5265 }
5266
5267 async fn join(
5268 &self,
5269 right: &dyn KrishivDataFrameOps,
5270 how: &str,
5271 left_on: &[&str],
5272 right_on: &[&str],
5273 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5274 let right_sql = right
5275 .as_any()
5276 .downcast_ref::<SqlDataFrame>()
5277 .ok_or_else(|| SqlError::DataFusion {
5278 message: "right DataFrame must be SqlDataFrame for join".into(),
5279 })?;
5280 use datafusion::common::JoinType;
5281 let join_type = match how.to_lowercase().as_str() {
5282 "inner" => JoinType::Inner,
5283 "left" => JoinType::Left,
5284 "right" => JoinType::Right,
5285 "full" | "outer" => JoinType::Full,
5286 "leftsemi" | "left_semi" => JoinType::LeftSemi,
5287 "rightsemi" | "right_semi" => JoinType::RightSemi,
5288 "leftanti" | "left_anti" => JoinType::LeftAnti,
5289 "rightanti" | "right_anti" => JoinType::RightAnti,
5290 _ => {
5291 return Err(SqlError::DataFusion {
5292 message: format!("unsupported join type: {how}"),
5293 });
5294 }
5295 };
5296 let df = self.dataframe.clone().join(
5297 right_sql.dataframe.clone(),
5298 join_type,
5299 left_on,
5300 right_on,
5301 None,
5302 )?;
5303 Ok(Box::new(self.with_new_dataframe(df, "join")))
5304 }
5305
5306 async fn union(
5307 &self,
5308 right: &dyn KrishivDataFrameOps,
5309 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5310 let right_sql = right
5311 .as_any()
5312 .downcast_ref::<SqlDataFrame>()
5313 .ok_or_else(|| SqlError::DataFusion {
5314 message: "right DataFrame must be SqlDataFrame for union".into(),
5315 })?;
5316 let df = self.dataframe.clone().union(right_sql.dataframe.clone())?;
5317 Ok(Box::new(self.with_new_dataframe(df, "union")))
5318 }
5319
5320 async fn union_distinct(
5321 &self,
5322 right: &dyn KrishivDataFrameOps,
5323 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5324 let right = sql_dataframe(right, "union_distinct")?;
5325 let df = self
5326 .dataframe
5327 .clone()
5328 .union_distinct(right.dataframe.clone())?;
5329 Ok(Box::new(self.with_new_dataframe(df, "union_distinct")))
5330 }
5331
5332 async fn intersect(
5333 &self,
5334 right: &dyn KrishivDataFrameOps,
5335 distinct: bool,
5336 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5337 let right = sql_dataframe(right, "intersect")?;
5338 let df = if distinct {
5339 self.dataframe
5340 .clone()
5341 .intersect_distinct(right.dataframe.clone())?
5342 } else {
5343 self.dataframe.clone().intersect(right.dataframe.clone())?
5344 };
5345 Ok(Box::new(self.with_new_dataframe(df, "intersect")))
5346 }
5347
5348 async fn except(
5349 &self,
5350 right: &dyn KrishivDataFrameOps,
5351 distinct: bool,
5352 ) -> SqlResult<Box<dyn KrishivDataFrameOps>> {
5353 let right = sql_dataframe(right, "except")?;
5354 let df = if distinct {
5355 self.dataframe
5356 .clone()
5357 .except_distinct(right.dataframe.clone())?
5358 } else {
5359 self.dataframe.clone().except(right.dataframe.clone())?
5360 };
5361 Ok(Box::new(self.with_new_dataframe(df, "except")))
5362 }
5363
5364 async fn register_batches(&self, name: &str, batches: Vec<RecordBatch>) -> SqlResult<()> {
5365 let schema = batches
5366 .first()
5367 .map(|b| b.schema())
5368 .unwrap_or_else(|| Arc::new(arrow::datatypes::Schema::empty()));
5369 let mem_table =
5370 datafusion::datasource::MemTable::try_new(schema, vec![batches]).map_err(|e| {
5371 SqlError::DataFusion {
5372 message: e.to_string(),
5373 }
5374 })?;
5375 self.context
5376 .register_table(name, Arc::new(mem_table))
5377 .map_err(SqlError::from)?;
5378 Ok(())
5379 }
5380
5381 async fn deregister_table(&self, name: &str) -> SqlResult<()> {
5382 let _ = self
5383 .context
5384 .deregister_table(name)
5385 .map_err(SqlError::from)?;
5386 Ok(())
5387 }
5388
5389 async fn create_view(&self, name: &str, replace: bool) -> SqlResult<()> {
5390 let query = self
5391 .query_text
5392 .as_deref()
5393 .ok_or_else(|| SqlError::DataFusion {
5394 message: "create_view requires an SQL query string on the DataFrame".into(),
5395 })?;
5396 let or_replace = if replace { "OR REPLACE " } else { "" };
5397 let safe_name = quote_identifier(name);
5398 let view_sql = format!("CREATE {or_replace}VIEW {safe_name} AS {query}");
5399 self.context.sql(&view_sql).await?;
5400 Ok(())
5401 }
5402}
5403
5404use krishiv_common::sql_util::quote_identifier;
5405
5406#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5413fn call_args_from_str(s: &str) -> Vec<String> {
5414 let mut args: Vec<String> = Vec::new();
5415 let mut cur = String::new();
5416 let mut in_str = false;
5417 let mut after_str = false;
5418 for ch in s.chars() {
5419 if after_str {
5420 if ch == ',' {
5421 after_str = false;
5422 }
5423 continue;
5424 }
5425 if in_str {
5426 if ch == '\'' {
5427 in_str = false;
5428 after_str = true;
5429 args.push(std::mem::take(&mut cur));
5430 } else {
5431 cur.push(ch);
5432 }
5433 } else if ch == '\'' {
5434 in_str = true;
5435 } else if ch == ',' {
5436 let t = cur.trim().to_string();
5437 if !t.is_empty() {
5438 args.push(t);
5439 }
5440 cur.clear();
5441 } else {
5442 cur.push(ch);
5443 }
5444 }
5445 let t = cur.trim().to_string();
5446 if !t.is_empty() {
5447 args.push(t);
5448 }
5449 args
5450}
5451
5452#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5459fn iceberg_table_ident(table_ref: &str) -> SqlResult<iceberg::TableIdent> {
5460 let parts: Vec<&str> = table_ref.splitn(3, '.').collect();
5461 match parts.len() {
5462 2 => {
5463 let ns = iceberg::NamespaceIdent::from_vec(vec![
5464 parts.first().copied().unwrap_or("").to_string(),
5465 ])
5466 .map_err(|e| SqlError::DataFusion {
5467 message: e.to_string(),
5468 })?;
5469 Ok(iceberg::TableIdent::new(
5470 ns,
5471 parts.get(1).copied().unwrap_or("").to_string(),
5472 ))
5473 }
5474 3 => {
5475 let ns = iceberg::NamespaceIdent::from_vec(vec![
5476 parts.get(1).copied().unwrap_or("").to_string(),
5477 ])
5478 .map_err(|e| SqlError::DataFusion {
5479 message: e.to_string(),
5480 })?;
5481 Ok(iceberg::TableIdent::new(
5482 ns,
5483 parts.get(2).copied().unwrap_or("").to_string(),
5484 ))
5485 }
5486 _ => Err(SqlError::DataFusion {
5487 message: format!(
5488 "invalid table reference '{table_ref}': expected 'ns.table' or 'cat.ns.table'"
5489 ),
5490 }),
5491 }
5492}
5493
5494#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5499fn parse_call_duration(s: &str) -> SqlResult<chrono::Duration> {
5500 let s = s.trim();
5501 let mut it = s.splitn(2, ' ');
5502 let n: i64 = it
5503 .next()
5504 .and_then(|v| v.parse().ok())
5505 .ok_or_else(|| SqlError::DataFusion {
5506 message: format!("invalid duration value in '{s}'"),
5507 })?;
5508 let unit = it.next().unwrap_or("").trim().to_ascii_lowercase();
5509 match unit.trim_end_matches('s') {
5510 "day" => Ok(chrono::Duration::days(n)),
5511 "hour" => Ok(chrono::Duration::hours(n)),
5512 "week" => Ok(chrono::Duration::weeks(n)),
5513 "minute" | "min" => Ok(chrono::Duration::minutes(n)),
5514 _ => Err(SqlError::DataFusion {
5515 message: format!("unknown duration unit '{unit}' in '{s}'"),
5516 }),
5517 }
5518}
5519
5520#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5528fn parse_dml_delete(stmt: &str) -> Option<(String, String)> {
5529 use datafusion::sql::sqlparser::ast::{FromTable, Statement, TableFactor};
5530 use datafusion::sql::sqlparser::dialect::GenericDialect;
5531 use datafusion::sql::sqlparser::parser::Parser;
5532
5533 let mut stmts = Parser::parse_sql(&GenericDialect {}, stmt).ok()?;
5534 if stmts.len() != 1 {
5535 return None;
5536 }
5537 let Statement::Delete(delete) = stmts.remove(0) else {
5538 return None;
5539 };
5540 let tables = match delete.from {
5543 FromTable::WithFromKeyword(tables) | FromTable::WithoutKeyword(tables) => tables,
5544 };
5545 let first_from = tables.into_iter().next()?;
5546 let table_name = match first_from.relation {
5547 TableFactor::Table { name, .. } => name.to_string(),
5548 _ => return None,
5549 };
5550 let predicate = delete
5551 .selection
5552 .map(|e| e.to_string())
5553 .unwrap_or_else(|| "TRUE".to_string());
5554 Some((table_name, predicate))
5555}
5556
5557#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5559struct ParsedInsert {
5560 table_ref: String,
5562 columns: Vec<String>,
5568 inner_query: String,
5571}
5572
5573#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5579fn parse_dml_insert(stmt: &str) -> Option<ParsedInsert> {
5580 use datafusion::sql::sqlparser::ast::{Statement, TableObject};
5581 use datafusion::sql::sqlparser::dialect::GenericDialect;
5582 use datafusion::sql::sqlparser::parser::Parser;
5583
5584 let mut stmts = Parser::parse_sql(&GenericDialect {}, stmt).ok()?;
5585 if stmts.len() != 1 {
5586 return None;
5587 }
5588 let Statement::Insert(insert) = stmts.remove(0) else {
5589 return None;
5590 };
5591 let TableObject::TableName(name) = insert.table else {
5592 return None;
5593 };
5594 let inner_query = insert.source?.to_string();
5595 Some(ParsedInsert {
5596 table_ref: name.to_string(),
5597 columns: insert.columns.iter().map(|c| c.to_string()).collect(),
5598 inner_query,
5599 })
5600}
5601
5602#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5604struct ParsedCtas {
5605 table_ref: String,
5607 or_replace: bool,
5608 inner_query: String,
5610 partition_by: Vec<String>,
5613}
5614
5615#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5625fn extract_partitioned_by(stmt: &str) -> Option<(String, Vec<String>)> {
5626 let bytes = stmt.as_bytes();
5627 let upper = stmt.to_ascii_uppercase();
5628 let upper_bytes = upper.as_bytes();
5629 const NEEDLE: &[u8] = b"PARTITIONED";
5630
5631 fn is_ident_byte(b: u8) -> bool {
5632 b.is_ascii_alphanumeric() || b == b'_'
5633 }
5634 fn skip_quoted(bytes: &[u8], mut i: usize, quote: u8) -> usize {
5637 i += 1;
5638 while let Some(&b) = bytes.get(i) {
5639 if b == quote {
5640 if bytes.get(i + 1) == Some("e) {
5641 i += 2;
5642 continue;
5643 }
5644 return i + 1;
5645 }
5646 i += 1;
5647 }
5648 i
5649 }
5650
5651 let mut i = 0;
5652 while let Some(&b) = bytes.get(i) {
5653 match b {
5654 b'\'' | b'"' => i = skip_quoted(bytes, i, b),
5655 _ => {
5656 let at_needle = upper_bytes
5657 .get(i..)
5658 .is_some_and(|rest| rest.starts_with(NEEDLE))
5659 && (i == 0
5660 || !i
5661 .checked_sub(1)
5662 .and_then(|p| upper_bytes.get(p))
5663 .copied()
5664 .is_some_and(is_ident_byte));
5665 if at_needle {
5666 let mut j = i + NEEDLE.len();
5667 while bytes.get(j).is_some_and(u8::is_ascii_whitespace) {
5668 j += 1;
5669 }
5670 if j > i + NEEDLE.len()
5673 && upper_bytes
5674 .get(j..)
5675 .is_some_and(|rest| rest.starts_with(b"BY"))
5676 && !upper_bytes.get(j + 2).copied().is_some_and(is_ident_byte)
5677 {
5678 let mut k = j + 2;
5679 while bytes.get(k).is_some_and(u8::is_ascii_whitespace) {
5680 k += 1;
5681 }
5682 if bytes.get(k) == Some(&b'(') {
5683 let mut depth = 0i32;
5685 let mut c = k;
5686 let close = loop {
5687 match bytes.get(c) {
5688 None => return None,
5690 Some(b'(') => depth += 1,
5691 Some(b')') => {
5692 depth -= 1;
5693 if depth == 0 {
5694 break c;
5695 }
5696 }
5697 Some(&(q @ b'\'' | q @ b'"')) => {
5698 c = skip_quoted(bytes, c, q);
5699 continue;
5700 }
5701 Some(_) => {}
5702 }
5703 c += 1;
5704 };
5705 let body = stmt.get(k + 1..close)?;
5706 let head = stmt.get(..i)?.trim_end();
5707 let tail = stmt.get(close + 1..)?.trim_start();
5708 let items = split_top_level_commas(body);
5709 let mut remainder = String::with_capacity(stmt.len());
5710 remainder.push_str(head);
5711 remainder.push(' ');
5712 remainder.push_str(tail);
5713 return Some((remainder, items));
5714 }
5715 }
5716 }
5717 i += 1;
5718 }
5719 }
5720 }
5721 None
5722}
5723
5724#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5727fn split_top_level_commas(s: &str) -> Vec<String> {
5728 let bytes = s.as_bytes();
5729 let mut items = Vec::new();
5730 let mut depth = 0i32;
5731 let mut start = 0usize;
5732 let mut i = 0;
5733 while let Some(&b) = bytes.get(i) {
5734 match b {
5735 b'(' => depth += 1,
5736 b')' => depth -= 1,
5737 b'\'' | b'"' => {
5738 i += 1;
5739 while bytes.get(i).is_some_and(|&c| c != b) {
5740 i += 1;
5741 }
5742 }
5743 b',' if depth == 0 => {
5744 if let Some(item) = s.get(start..i).map(str::trim)
5745 && !item.is_empty()
5746 {
5747 items.push(item.to_string());
5748 }
5749 start = i + 1;
5750 }
5751 _ => {}
5752 }
5753 i += 1;
5754 }
5755 if let Some(last) = s.get(start..).map(str::trim)
5756 && !last.is_empty()
5757 {
5758 items.push(last.to_string());
5759 }
5760 items
5761}
5762
5763fn split_sql_statements(sql: &str) -> Vec<String> {
5771 let mut items = Vec::new();
5772 let mut start = 0usize;
5773 let mut chars = sql.char_indices().peekable();
5774 while let Some((i, c)) = chars.next() {
5775 match c {
5776 '\'' => {
5777 while let Some((_, c2)) = chars.next() {
5779 if c2 == '\'' {
5780 if chars.peek().is_some_and(|&(_, c3)| c3 == '\'') {
5781 chars.next();
5782 continue;
5783 }
5784 break;
5785 }
5786 }
5787 }
5788 '"' => {
5789 for (_, c2) in chars.by_ref() {
5790 if c2 == '"' {
5791 break;
5792 }
5793 }
5794 }
5795 '-' if chars.peek().is_some_and(|&(_, c2)| c2 == '-') => {
5796 for (_, c2) in chars.by_ref() {
5797 if c2 == '\n' {
5798 break;
5799 }
5800 }
5801 }
5802 '/' if chars.peek().is_some_and(|&(_, c2)| c2 == '*') => {
5803 chars.next();
5804 let mut star = false;
5805 for (_, c2) in chars.by_ref() {
5806 if star && c2 == '/' {
5807 break;
5808 }
5809 star = c2 == '*';
5810 }
5811 }
5812 ';' => {
5813 if let Some(piece) = sql.get(start..i).map(str::trim)
5814 && !piece.is_empty()
5815 {
5816 items.push(piece.to_string());
5817 }
5818 start = i + 1;
5820 }
5821 _ => {}
5822 }
5823 }
5824 if let Some(last) = sql.get(start..).map(str::trim)
5825 && !last.is_empty()
5826 {
5827 items.push(last.to_string());
5828 }
5829 items
5830}
5831
5832#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5840fn parse_ctas(stmt: &str) -> Option<ParsedCtas> {
5841 use datafusion::sql::sqlparser::ast::Statement;
5842 use datafusion::sql::sqlparser::dialect::GenericDialect;
5843 use datafusion::sql::sqlparser::parser::Parser;
5844
5845 let (stripped, partition_by) = match extract_partitioned_by(stmt) {
5846 Some((remainder, items)) => (remainder, items),
5847 None => (stmt.to_string(), Vec::new()),
5848 };
5849 let mut stmts = Parser::parse_sql(&GenericDialect {}, &stripped).ok()?;
5850 if stmts.len() != 1 {
5851 return None;
5852 }
5853 let Statement::CreateTable(create) = stmts.remove(0) else {
5854 return None;
5855 };
5856 if create.external || create.temporary {
5857 return None;
5858 }
5859 let inner_query = create.query?.to_string();
5860 Some(ParsedCtas {
5861 table_ref: create.name.to_string(),
5862 or_replace: create.or_replace,
5863 inner_query,
5864 partition_by,
5865 })
5866}
5867
5868#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5870struct ParsedUpdate {
5871 table_ref: String,
5872 assignments: Vec<(String, String)>,
5874 predicate: Option<String>,
5875}
5876
5877#[cfg(all(feature = "iceberg-datafusion", feature = "local-catalog"))]
5883fn parse_dml_update(stmt: &str) -> Option<ParsedUpdate> {
5884 use datafusion::sql::sqlparser::ast::{Statement, TableFactor};
5885 use datafusion::sql::sqlparser::dialect::GenericDialect;
5886 use datafusion::sql::sqlparser::parser::Parser;
5887
5888 let mut stmts = Parser::parse_sql(&GenericDialect {}, stmt).ok()?;
5889 if stmts.len() != 1 {
5890 return None;
5891 }
5892 let Statement::Update(update) = stmts.remove(0) else {
5894 return None;
5895 };
5896 let table_name = match update.table.relation {
5897 TableFactor::Table { name, .. } => name.to_string(),
5898 _ => return None,
5899 };
5900 let parsed_assignments: Vec<(String, String)> = update
5902 .assignments
5903 .into_iter()
5904 .map(|a| {
5905 let col = a.target.to_string();
5907 let val = a.value.to_string();
5908 (col, val)
5909 })
5910 .collect();
5911 if parsed_assignments.is_empty() {
5912 return None;
5913 }
5914 Some(ParsedUpdate {
5915 table_ref: table_name,
5916 assignments: parsed_assignments,
5917 predicate: update.selection.map(|e| e.to_string()),
5918 })
5919}
5920
5921pub fn plan_sql(query: impl Into<String>) -> SqlResult<SqlPlan> {
5923 let query = query.into();
5924 if query.trim().is_empty() {
5925 return Err(SqlError::EmptyQuery);
5926 }
5927
5928 if let Some(stmt) = cep_sql::parse_match_recognize(&query)? {
5929 let logical_plan = cep_sql::plan_match_recognize(stmt, &query);
5930 let optimized = Optimizer::default().optimize(logical_plan)?;
5931 return Ok(SqlPlan {
5932 query,
5933 logical_plan: optimized.plan,
5934 });
5935 }
5936
5937 let logical_plan =
5938 LogicalPlan::new("sql-query", ExecutionKind::Batch).with_node(PlanNode::new(
5939 "sql",
5940 format!("sql: {}", query.trim()),
5941 ExecutionKind::Batch,
5942 ));
5943
5944 let optimized = Optimizer::default().optimize(logical_plan)?;
5945 Ok(SqlPlan {
5946 query,
5947 logical_plan: optimized.plan,
5948 })
5949}
5950
5951pub fn explain_sql(query: impl Into<String>) -> SqlResult<String> {
5953 let plan = plan_sql(query)?;
5954 Ok(plan.logical_plan().describe())
5955}
5956
5957pub fn explain_sql_optimized(query: impl Into<String>, optimizer: &Optimizer) -> SqlResult<String> {
5962 let plan = plan_sql(query)?;
5963 let result = optimizer.optimize(plan.logical_plan().clone())?;
5964 let mut output = result.plan.describe();
5965 let optimizer_line = result.describe();
5966 output.push('\n');
5967 output.push_str(&optimizer_line);
5968 Ok(output)
5969}
5970
5971pub fn explain_sql_with_cost(
5973 query: impl Into<String>,
5974 cost_model: &dyn CostModel,
5975) -> SqlResult<String> {
5976 let plan = plan_sql(query)?;
5977 let cost = cost_model.estimate(plan.logical_plan());
5978 let mut output = plan.logical_plan().describe();
5979 output.push_str(&format!(
5980 "\ncost: cpu_nanos={}, memory_bytes={}, network_bytes={}",
5981 cost.cpu_nanos, cost.memory_bytes, cost.network_bytes
5982 ));
5983 Ok(output)
5984}
5985
5986pub fn referenced_table_names(query: impl AsRef<str>) -> SqlResult<Vec<String>> {
5992 let query = query.as_ref();
5993 if query.trim().is_empty() {
5994 return Err(SqlError::EmptyQuery);
5995 }
5996
5997 let statements =
5998 Parser::parse_sql(&GenericDialect {}, query).map_err(|e| SqlError::DataFusion {
5999 message: format!("SQL parse error: {e}"),
6000 })?;
6001 let mut names = BTreeSet::new();
6002 let _ = visit_relations(&statements, |relation| {
6003 names.insert(relation.to_string());
6004 ControlFlow::<()>::Continue(())
6005 });
6006 Ok(names.into_iter().collect())
6007}
6008
6009pub fn pretty_batches(batches: &[RecordBatch]) -> SqlResult<String> {
6011 Ok(pretty_format_batches(batches)
6012 .map_err(|error| SqlError::DataFusion {
6013 message: error.to_string(),
6014 })?
6015 .to_string())
6016}
6017
6018#[cfg(test)]
6019mod sql_tests;