1use super::memory::SourceTaskMemoryModel;
2use crate::{
3 error::{QueryError, QueryErrorCode, Result as QueryResult},
4 execution_contracts::{MemoryControlPolicy, SourceChunkPlanningMode},
5 source::{LocalParquetFile, LocalParquetSource, ParquetReadStrategy},
6};
7
8pub const LOCAL_PARQUET_THROUGHPUT_MAX_ROW_GROUPS_PER_CHUNK: usize =
12 SourceChunkPlanningOptions::DEFAULT_LOCAL_PARQUET_THROUGHPUT_MAX_ROW_GROUPS_PER_CHUNK;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[non_exhaustive]
21pub struct SourceChunkPlanningOptions {
22 local_parquet_throughput_max_row_groups_per_chunk: usize,
23}
24
25impl SourceChunkPlanningOptions {
26 pub(crate) const DEFAULT_LOCAL_PARQUET_THROUGHPUT_MAX_ROW_GROUPS_PER_CHUNK: usize = 1024;
27
28 pub(crate) fn local_parquet_throughput_max_row_groups_per_chunk(&self) -> usize {
29 self.local_parquet_throughput_max_row_groups_per_chunk
30 }
31
32 pub fn with_local_parquet_throughput_max_row_groups_per_chunk(mut self, row_groups: usize) -> Self {
36 self.local_parquet_throughput_max_row_groups_per_chunk = row_groups;
37 self
38 }
39
40 pub fn validate(&self) -> QueryResult<()> {
42 validate_source_chunk_planning_nonzero_usize(
43 "local_parquet_throughput_max_row_groups_per_chunk",
44 self.local_parquet_throughput_max_row_groups_per_chunk,
45 )
46 }
47}
48
49impl Default for SourceChunkPlanningOptions {
50 fn default() -> Self {
51 Self {
52 local_parquet_throughput_max_row_groups_per_chunk:
53 Self::DEFAULT_LOCAL_PARQUET_THROUGHPUT_MAX_ROW_GROUPS_PER_CHUNK,
54 }
55 }
56}
57
58fn validate_source_chunk_planning_nonzero_usize(field: &'static str, value: usize) -> QueryResult<()> {
59 if value == 0 {
60 return Err(source_chunk_planning_zero_error(field, value));
61 }
62 Ok(())
63}
64
65fn source_chunk_planning_zero_error(field: &'static str, value: impl std::fmt::Display) -> QueryError {
66 QueryError::new(
67 QueryErrorCode::InvalidConfiguration,
68 "query_engine.source_chunk_planning_options.validate",
69 "source chunk planning option must be greater than zero",
70 )
71 .with_fact("field", field)
72 .with_fact("value", value)
73 .with_fact("reason", "zero")
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct SourceChunkPlanningPolicy {
79 pub max_source_chunk_bytes: Option<u64>,
86 pub max_row_groups_per_chunk: usize,
91 source_chunk_planning_mode: SourceChunkPlanningMode,
92 source_chunk_planning_reason: &'static str,
93}
94
95impl SourceChunkPlanningPolicy {
96 pub const fn new(max_source_chunk_bytes: Option<u64>, max_row_groups_per_chunk: usize) -> Self {
97 Self {
98 max_source_chunk_bytes,
99 max_row_groups_per_chunk,
100 source_chunk_planning_mode: SourceChunkPlanningMode::Conservative,
101 source_chunk_planning_reason: "explicit_policy",
102 }
103 }
104
105 pub(crate) fn source_chunk_planning_mode(self) -> SourceChunkPlanningMode {
106 self.source_chunk_planning_mode
107 }
108
109 pub fn source_chunk_planning_reason(self) -> &'static str {
110 self.source_chunk_planning_reason
111 }
112
113 pub fn mark_downstream_cap(&mut self, reason: &'static str) {
114 self.source_chunk_planning_reason = reason;
115 }
116
117 pub fn for_source_task(memory_policy: MemoryControlPolicy, model: SourceTaskMemoryModel) -> Self {
125 let base = Self::from(memory_policy);
126 let MemoryControlPolicy { device_budget_bytes, max_source_chunk_bytes, max_row_groups_per_chunk, .. } =
127 memory_policy;
128 if max_source_chunk_bytes > device_budget_bytes
129 && max_source_chunk_bytes != MemoryControlPolicy::DEFAULT_MAX_SOURCE_CHUNK_BYTES
130 {
131 return base;
132 }
133 let budget_target = device_budget_bytes / model.live_slot_count().max(1);
134 let task_target = max_source_chunk_bytes.min(budget_target.max(1));
135 let mut policy = Self::new(Some(task_target), max_row_groups_per_chunk);
136 policy.source_chunk_planning_reason =
137 if task_target < max_source_chunk_bytes { "memory_cap" } else { "configured_policy" };
138 policy
139 }
140
141 pub fn for_source_task_with_mode_and_options(
143 source: &LocalParquetSource,
144 memory_policy: MemoryControlPolicy,
145 model: SourceTaskMemoryModel,
146 mode: SourceChunkPlanningMode,
147 options: SourceChunkPlanningOptions,
148 ) -> Self {
149 Self::for_source_task(memory_policy, model).with_source_planning_mode(source, mode, options)
150 }
151
152 pub fn from_memory_policy_with_mode(
154 source: &LocalParquetSource,
155 memory_policy: MemoryControlPolicy,
156 mode: SourceChunkPlanningMode,
157 ) -> Self {
158 Self::from_memory_policy_with_mode_and_options(
159 source,
160 memory_policy,
161 mode,
162 SourceChunkPlanningOptions::default(),
163 )
164 }
165
166 pub fn from_memory_policy_with_mode_and_options(
168 source: &LocalParquetSource,
169 memory_policy: MemoryControlPolicy,
170 mode: SourceChunkPlanningMode,
171 options: SourceChunkPlanningOptions,
172 ) -> Self {
173 Self::from(memory_policy).with_source_planning_mode(source, mode, options)
174 }
175
176 pub(super) fn effective_max_row_groups_per_chunk(self) -> usize {
177 self.max_row_groups_per_chunk.max(1)
178 }
179
180 fn with_source_planning_mode(
181 mut self,
182 source: &LocalParquetSource,
183 mode: SourceChunkPlanningMode,
184 options: SourceChunkPlanningOptions,
185 ) -> Self {
186 if mode != SourceChunkPlanningMode::LocalParquetThroughput {
187 return self;
188 }
189 self.source_chunk_planning_mode = mode;
190 if !local_parquet_throughput_planning_admitted(source, self) {
191 self.source_chunk_planning_reason = "local_parquet_throughput_rejected";
192 return self;
193 }
194 self.max_row_groups_per_chunk =
195 self.max_row_groups_per_chunk.max(options.local_parquet_throughput_max_row_groups_per_chunk());
196 self.source_chunk_planning_reason = "local_parquet_throughput_admitted";
197 self
198 }
199}
200
201impl Default for SourceChunkPlanningPolicy {
202 fn default() -> Self {
203 Self {
204 max_source_chunk_bytes: None,
205 max_row_groups_per_chunk: usize::MAX,
206 source_chunk_planning_mode: SourceChunkPlanningMode::Conservative,
207 source_chunk_planning_reason: "unbounded_policy",
208 }
209 }
210}
211
212impl From<MemoryControlPolicy> for SourceChunkPlanningPolicy {
213 fn from(memory_policy: MemoryControlPolicy) -> Self {
214 let target = if memory_policy.max_source_chunk_bytes == MemoryControlPolicy::DEFAULT_MAX_SOURCE_CHUNK_BYTES {
215 (memory_policy.device_budget_bytes / SourceTaskMemoryModel::aggregate_pipeline(false).live_slot_count())
216 .max(1)
217 } else {
218 memory_policy.max_source_chunk_bytes
219 };
220 let mut policy = Self::new(Some(target), memory_policy.max_row_groups_per_chunk);
221 policy.source_chunk_planning_reason =
222 if memory_policy.max_source_chunk_bytes == MemoryControlPolicy::DEFAULT_MAX_SOURCE_CHUNK_BYTES {
223 "memory_cap"
224 } else {
225 "configured_policy"
226 };
227 policy
228 }
229}
230
231fn local_parquet_throughput_planning_admitted(source: &LocalParquetSource, policy: SourceChunkPlanningPolicy) -> bool {
235 if policy.max_row_groups_per_chunk != 1 || policy.max_source_chunk_bytes.is_none() {
236 return false;
237 }
238 if source.files.is_empty()
239 || source.is_zero_column_count_source()
240 || source.scan_limit.is_some()
241 || source.read_columns.is_empty()
242 || !source.partition_columns.is_empty()
243 {
244 return false;
245 }
246 source.files.iter().all(|file| {
247 local_parquet_throughput_file_uses_supported_reader_path(file)
248 && file.partition_values.is_empty()
249 && matches!(
250 file.resolved_source.read_strategy(),
251 ParquetReadStrategy::LocalKvikio | ParquetReadStrategy::LocalCacheKvikio
252 )
253 && local_parquet_throughput_file_has_source_read_bytes(source, file)
254 })
255}
256
257fn local_parquet_throughput_file_uses_supported_reader_path(file: &LocalParquetFile) -> bool {
258 match file.resolved_source.read_strategy() {
259 ParquetReadStrategy::LocalKvikio => file.location.is_local_path(),
260 ParquetReadStrategy::LocalCacheKvikio => file.path.is_file(),
261 ParquetReadStrategy::RemoteDirect => false,
262 }
263}
264
265fn local_parquet_throughput_file_has_source_read_bytes(source: &LocalParquetSource, file: &LocalParquetFile) -> bool {
266 let selected_row_groups = match &file.row_groups {
267 Some(row_groups) if !row_groups.is_empty() => row_groups.clone(),
268 Some(_) => return false,
269 None => {
270 let Some(count) = file.projected_row_group_uncompressed_bytes.as_ref().map(Vec::len) else {
271 return false;
272 };
273 if count == 0 {
274 return false;
275 }
276 (0..count).collect()
277 },
278 };
279 let Some(max_row_group) = selected_row_groups.iter().copied().max() else {
280 return false;
281 };
282 let projected_covers_selection =
283 file.projected_row_group_uncompressed_bytes.as_ref().is_some_and(|bytes| max_row_group < bytes.len());
284 let pure_filter_covers_selection = source.pure_filter_columns.is_empty()
285 || file.pure_filter_row_group_uncompressed_bytes.as_ref().is_some_and(|bytes| max_row_group < bytes.len());
286 projected_covers_selection && pure_filter_covers_selection
287}