1use super::{
7 fmt, take, Arc, Array, Boundedness, ColumnId, DataFusionError, Debug, DisplayAs,
8 DisplayFormatType, EmissionType, EquivalenceProperties, ExecutionPlan, Formatter, LexOrdering,
9 LookupJoinType, LookupMemoryCache, LookupSourceDyn, Partitioning, PlanProperties, RecordBatch,
10 RecordBatchStreamAdapter, Result, RowConverter, Schema, SchemaRef, Semaphore,
11 SendableRecordBatchStream, SortField, StreamExt, TaskContext, UInt32Array,
12 LOOKUP_SOURCE_TIMEOUT,
13};
14
15pub struct PartialLookupJoinExec {
19 input: Arc<dyn ExecutionPlan>,
20 lookup_cache: Arc<LookupMemoryCache>,
21 stream_key_indices: Vec<usize>,
22 join_type: LookupJoinType,
23 schema: SchemaRef,
24 properties: Arc<PlanProperties>,
25 converter: Arc<RowConverter>,
28 stream_field_count: usize,
29 lookup_schema: SchemaRef,
30 source: Option<Arc<dyn LookupSourceDyn>>,
31 fetch_semaphore: Arc<Semaphore>,
32 projection: Vec<ColumnId>,
33}
34
35impl PartialLookupJoinExec {
36 pub fn try_new(
42 input: Arc<dyn ExecutionPlan>,
43 lookup_cache: Arc<LookupMemoryCache>,
44 stream_key_indices: Vec<usize>,
45 key_sort_fields: Vec<SortField>,
46 join_type: LookupJoinType,
47 lookup_schema: SchemaRef,
48 output_schema: SchemaRef,
49 ) -> Result<Self> {
50 Self::try_new_with_source(
51 input,
52 lookup_cache,
53 stream_key_indices,
54 key_sort_fields,
55 join_type,
56 lookup_schema,
57 output_schema,
58 None,
59 Arc::new(Semaphore::new(64)),
60 vec![],
61 )
62 }
63
64 #[allow(clippy::too_many_arguments)]
70 pub fn try_new_with_source(
71 input: Arc<dyn ExecutionPlan>,
72 lookup_cache: Arc<LookupMemoryCache>,
73 stream_key_indices: Vec<usize>,
74 key_sort_fields: Vec<SortField>,
75 join_type: LookupJoinType,
76 lookup_schema: SchemaRef,
77 output_schema: SchemaRef,
78 source: Option<Arc<dyn LookupSourceDyn>>,
79 fetch_semaphore: Arc<Semaphore>,
80 projection: Vec<ColumnId>,
81 ) -> Result<Self> {
82 let output_schema = if join_type == LookupJoinType::LeftOuter {
83 let stream_count = input.schema().fields().len();
84 let mut fields = output_schema.fields().to_vec();
85 for f in &mut fields[stream_count..] {
86 if !f.is_nullable() {
87 *f = Arc::new(f.as_ref().clone().with_nullable(true));
88 }
89 }
90 Arc::new(Schema::new_with_metadata(
91 fields,
92 output_schema.metadata().clone(),
93 ))
94 } else {
95 output_schema
96 };
97
98 let properties = Arc::new(PlanProperties::new(
99 EquivalenceProperties::new(Arc::clone(&output_schema)),
100 Partitioning::UnknownPartitioning(1),
101 EmissionType::Incremental,
102 Boundedness::Unbounded {
103 requires_infinite_memory: false,
104 },
105 ));
106
107 let stream_field_count = input.schema().fields().len();
108 let converter = Arc::new(RowConverter::new(key_sort_fields)?);
109
110 Ok(Self {
111 input,
112 lookup_cache,
113 stream_key_indices,
114 join_type,
115 schema: output_schema,
116 properties,
117 converter,
118 stream_field_count,
119 lookup_schema,
120 source,
121 fetch_semaphore,
122 projection,
123 })
124 }
125}
126
127impl Debug for PartialLookupJoinExec {
128 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
129 f.debug_struct("PartialLookupJoinExec")
130 .field("join_type", &self.join_type)
131 .field("stream_keys", &self.stream_key_indices)
132 .field("cache_table_id", &self.lookup_cache.table_id())
133 .finish_non_exhaustive()
134 }
135}
136
137impl DisplayAs for PartialLookupJoinExec {
138 fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter<'_>) -> fmt::Result {
139 match t {
140 DisplayFormatType::Default | DisplayFormatType::Verbose => {
141 write!(
142 f,
143 "PartialLookupJoinExec: type={}, stream_keys={:?}, cache_entries={}",
144 self.join_type,
145 self.stream_key_indices,
146 self.lookup_cache.len(),
147 )
148 }
149 DisplayFormatType::TreeRender => write!(f, "PartialLookupJoinExec"),
150 }
151 }
152}
153
154impl ExecutionPlan for PartialLookupJoinExec {
155 fn as_any(&self) -> &dyn std::any::Any {
156 self
157 }
158
159 fn name(&self) -> &'static str {
160 "PartialLookupJoinExec"
161 }
162
163 fn schema(&self) -> SchemaRef {
164 Arc::clone(&self.schema)
165 }
166
167 fn properties(&self) -> &Arc<PlanProperties> {
168 &self.properties
169 }
170
171 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
172 vec![&self.input]
173 }
174
175 fn with_new_children(
176 self: Arc<Self>,
177 mut children: Vec<Arc<dyn ExecutionPlan>>,
178 ) -> Result<Arc<dyn ExecutionPlan>> {
179 if children.len() != 1 {
180 return Err(DataFusionError::Plan(
181 "PartialLookupJoinExec requires exactly one child".into(),
182 ));
183 }
184 Ok(Arc::new(Self {
185 input: children.swap_remove(0),
186 lookup_cache: Arc::clone(&self.lookup_cache),
187 stream_key_indices: self.stream_key_indices.clone(),
188 join_type: self.join_type,
189 schema: Arc::clone(&self.schema),
190 properties: self.properties.clone(),
191 converter: Arc::clone(&self.converter),
192 stream_field_count: self.stream_field_count,
193 lookup_schema: Arc::clone(&self.lookup_schema),
194 source: self.source.clone(),
195 fetch_semaphore: Arc::clone(&self.fetch_semaphore),
196 projection: self.projection.clone(),
197 }))
198 }
199
200 fn execute(
201 &self,
202 partition: usize,
203 context: Arc<TaskContext>,
204 ) -> Result<SendableRecordBatchStream> {
205 let input_stream = self.input.execute(partition, context)?;
206 let converter = Arc::clone(&self.converter);
207 let lookup_cache = Arc::clone(&self.lookup_cache);
208 let stream_key_indices = self.stream_key_indices.clone();
209 let join_type = self.join_type;
210 let schema = self.schema();
211 let stream_field_count = self.stream_field_count;
212 let lookup_schema = Arc::clone(&self.lookup_schema);
213 let source = self.source.clone();
214 let fetch_semaphore = Arc::clone(&self.fetch_semaphore);
215 let projection = self.projection.clone();
216
217 let output = input_stream.then(move |result| {
218 let lookup_cache = Arc::clone(&lookup_cache);
219 let converter = Arc::clone(&converter);
220 let stream_key_indices = stream_key_indices.clone();
221 let schema = Arc::clone(&schema);
222 let lookup_schema = Arc::clone(&lookup_schema);
223 let source = source.clone();
224 let fetch_semaphore = Arc::clone(&fetch_semaphore);
225 let projection = projection.clone();
226 async move {
227 let batch = result?;
228 if batch.num_rows() == 0 {
229 return Ok(RecordBatch::new_empty(Arc::clone(&schema)));
230 }
231 probe_partial_batch_with_fallback(
232 &batch,
233 &converter,
234 &lookup_cache,
235 &stream_key_indices,
236 join_type,
237 &schema,
238 stream_field_count,
239 &lookup_schema,
240 source.as_deref(),
241 &fetch_semaphore,
242 &projection,
243 )
244 .await
245 }
246 });
247
248 Ok(Box::pin(RecordBatchStreamAdapter::new(
249 self.schema(),
250 output,
251 )))
252 }
253}
254
255impl datafusion::physical_plan::ExecutionPlanProperties for PartialLookupJoinExec {
256 fn output_partitioning(&self) -> &Partitioning {
257 self.properties.output_partitioning()
258 }
259
260 fn output_ordering(&self) -> Option<&LexOrdering> {
261 self.properties.output_ordering()
262 }
263
264 fn boundedness(&self) -> Boundedness {
265 Boundedness::Unbounded {
266 requires_infinite_memory: false,
267 }
268 }
269
270 fn pipeline_behavior(&self) -> EmissionType {
271 EmissionType::Incremental
272 }
273
274 fn equivalence_properties(&self) -> &EquivalenceProperties {
275 self.properties.equivalence_properties()
276 }
277}
278
279#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
286async fn probe_partial_batch_with_fallback(
287 stream_batch: &RecordBatch,
288 converter: &RowConverter,
289 lookup_cache: &LookupMemoryCache,
290 stream_key_indices: &[usize],
291 join_type: LookupJoinType,
292 output_schema: &SchemaRef,
293 stream_field_count: usize,
294 lookup_schema: &SchemaRef,
295 source: Option<&dyn LookupSourceDyn>,
296 fetch_semaphore: &Semaphore,
297 projection: &[ColumnId],
298) -> Result<RecordBatch> {
299 let key_cols: Vec<_> = stream_key_indices
300 .iter()
301 .map(|&i| stream_batch.column(i).clone())
302 .collect();
303 let rows = converter.convert_columns(&key_cols)?;
304
305 let num_rows = stream_batch.num_rows();
306 let mut stream_indices: Vec<u32> = Vec::with_capacity(num_rows);
307 let mut lookup_batches: Vec<Option<RecordBatch>> = Vec::with_capacity(num_rows);
308 let mut miss_keys: Vec<(usize, Vec<u8>)> = Vec::new();
309
310 #[allow(clippy::cast_possible_truncation)]
311 for row in 0..num_rows {
312 if key_cols.iter().any(|c| c.is_null(row)) {
314 if join_type == LookupJoinType::LeftOuter {
315 stream_indices.push(row as u32);
316 lookup_batches.push(None);
317 }
318 continue;
319 }
320
321 let key = rows.row(row);
322 let result = lookup_cache.get_cached(key.as_ref());
323 if let Some(batch) = result.into_batch() {
324 stream_indices.push(row as u32);
325 lookup_batches.push(Some(batch));
326 } else {
327 let idx = stream_indices.len();
328 stream_indices.push(row as u32);
329 lookup_batches.push(None);
330 miss_keys.push((idx, key.as_ref().to_vec()));
331 }
332 }
333
334 if let Some(source) = source {
336 if !miss_keys.is_empty() {
337 let _permit = fetch_semaphore
338 .acquire()
339 .await
340 .map_err(|_| DataFusionError::Internal("fetch semaphore closed".into()))?;
341
342 let key_refs: Vec<&[u8]> = miss_keys.iter().map(|(_, k)| k.as_slice()).collect();
343
344 let results = match tokio::time::timeout(
348 LOOKUP_SOURCE_TIMEOUT,
349 source.query_batch(&key_refs, &[], projection),
350 )
351 .await
352 {
353 Ok(Ok(results)) => results,
354 Ok(Err(e)) => {
355 return Err(DataFusionError::Execution(format!(
356 "lookup source query failed ({} keys): {e}",
357 miss_keys.len()
358 )));
359 }
360 Err(_elapsed) => {
361 return Err(DataFusionError::Execution(format!(
362 "lookup source query timed out after {LOOKUP_SOURCE_TIMEOUT:?} \
363 ({} keys)",
364 miss_keys.len()
365 )));
366 }
367 };
368
369 if results.len() != miss_keys.len() {
370 return Err(DataFusionError::Execution(format!(
371 "Lookup source returned mismatched results cardinality. Expected {} results, but got {} results. Miss keys: {:?}",
372 miss_keys.len(),
373 results.len(),
374 miss_keys
375 )));
376 }
377
378 for ((idx, key_bytes), maybe_batch) in miss_keys.iter().zip(results) {
379 if let Some(batch) = maybe_batch {
380 lookup_cache.insert(key_bytes, batch.clone());
381 lookup_batches[*idx] = Some(batch);
382 }
383 }
384 }
385 }
386
387 if join_type == LookupJoinType::Inner {
389 let mut write = 0;
390 for read in 0..stream_indices.len() {
391 if lookup_batches[read].is_some() {
392 stream_indices[write] = stream_indices[read];
393 lookup_batches.swap(write, read);
394 write += 1;
395 }
396 }
397 stream_indices.truncate(write);
398 lookup_batches.truncate(write);
399 }
400
401 if stream_indices.is_empty() {
402 return Ok(RecordBatch::new_empty(Arc::clone(output_schema)));
403 }
404
405 let take_indices = UInt32Array::from(stream_indices);
406 let mut columns = Vec::with_capacity(output_schema.fields().len());
407
408 for col in stream_batch.columns() {
409 columns.push(take(col.as_ref(), &take_indices, None)?);
410 }
411
412 let lookup_col_count = lookup_schema.fields().len();
413 for col_idx in 0..lookup_col_count {
414 let arrays: Vec<_> = lookup_batches
415 .iter()
416 .map(|opt| match opt {
417 Some(b) => b.column(col_idx).clone(),
418 None => arrow_array::new_null_array(lookup_schema.field(col_idx).data_type(), 1),
419 })
420 .collect();
421 let refs: Vec<&dyn arrow_array::Array> = arrays.iter().map(AsRef::as_ref).collect();
422 columns.push(arrow::compute::concat(&refs)?);
423 }
424
425 debug_assert_eq!(
426 columns.len(),
427 stream_field_count + lookup_col_count,
428 "output column count mismatch"
429 );
430
431 Ok(RecordBatch::try_new(Arc::clone(output_schema), columns)?)
432}