Skip to main content

datafusion_catalog/memory/
table.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! [`MemTable`] for querying `Vec<RecordBatch>` by DataFusion.
19
20use std::collections::HashMap;
21use std::fmt::Debug;
22use std::sync::Arc;
23
24use crate::TableProvider;
25
26use arrow::array::{
27    Array, ArrayRef, BooleanArray, RecordBatch as ArrowRecordBatch, UInt64Array,
28};
29use arrow::compute::kernels::zip::zip;
30use arrow::compute::{and, filter_record_batch};
31use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
32use arrow::record_batch::RecordBatch;
33use datafusion_common::error::Result;
34use datafusion_common::tree_node::TreeNodeRecursion;
35use datafusion_common::{Constraints, DFSchema, SchemaExt, not_impl_err, plan_err};
36use datafusion_datasource::memory::{MemSink, MemorySourceConfig};
37use datafusion_datasource::sink::DataSinkExec;
38use datafusion_datasource::source::DataSourceExec;
39use datafusion_expr::dml::InsertOp;
40use datafusion_expr::physical_planning_context::PhysicalPlanningContext;
41use datafusion_expr::{Expr, SortExpr, TableType};
42use datafusion_physical_expr::{
43    LexOrdering, create_physical_expr, create_physical_sort_exprs,
44};
45use datafusion_physical_plan::repartition::RepartitionExec;
46use datafusion_physical_plan::stream::RecordBatchStreamAdapter;
47use datafusion_physical_plan::{
48    ChildrenPropertiesMode, DisplayAs, DisplayFormatType, ExecutionPlan, Partitioning,
49    PhysicalExpr, PlanProperties, ReplaceChildrenOptions, collect_partitioned,
50};
51use datafusion_session::Session;
52
53use async_trait::async_trait;
54use log::debug;
55use parking_lot::Mutex;
56use tokio::sync::RwLock;
57
58// backward compatibility
59pub use datafusion_datasource::memory::PartitionData;
60
61/// In-memory data source for presenting a `Vec<RecordBatch>` as a
62/// data source that can be queried by DataFusion. This allows data to
63/// be pre-loaded into memory and then repeatedly queried without
64/// incurring additional file I/O overhead.
65#[derive(Debug)]
66pub struct MemTable {
67    schema: SchemaRef,
68    // batches used to be pub(crate), but it's needed to be public for the tests
69    pub batches: Vec<PartitionData>,
70    constraints: Constraints,
71    column_defaults: HashMap<String, Expr>,
72    /// Optional pre-known sort order(s). Must be `SortExpr`s.
73    /// inserting data into this table removes the order
74    pub sort_order: Arc<Mutex<Vec<Vec<SortExpr>>>>,
75}
76
77impl MemTable {
78    /// Create a new in-memory table from the provided schema and record batches.
79    ///
80    /// Requires at least one partition. To construct an empty `MemTable`, pass
81    /// `vec![vec![]]` as the `partitions` argument, this represents one partition with
82    /// no batches.
83    pub fn try_new(schema: SchemaRef, partitions: Vec<Vec<RecordBatch>>) -> Result<Self> {
84        if partitions.is_empty() {
85            return plan_err!("No partitions provided, expected at least one partition");
86        }
87
88        for batches in partitions.iter().flatten() {
89            let batches_schema = batches.schema();
90            if !schema.contains(&batches_schema) {
91                debug!(
92                    "mem table schema does not contain batches schema. \
93                        Target_schema: {schema:?}. Batches Schema: {batches_schema:?}"
94                );
95                return plan_err!("Mismatch between schema and batches");
96            }
97        }
98
99        Ok(Self {
100            schema,
101            batches: partitions
102                .into_iter()
103                .map(|e| Arc::new(RwLock::new(e)))
104                .collect::<Vec<_>>(),
105            constraints: Constraints::default(),
106            column_defaults: HashMap::new(),
107            sort_order: Arc::new(Mutex::new(vec![])),
108        })
109    }
110
111    /// Assign constraints
112    pub fn with_constraints(mut self, constraints: Constraints) -> Self {
113        self.constraints = constraints;
114        self
115    }
116
117    /// Assign column defaults
118    pub fn with_column_defaults(
119        mut self,
120        column_defaults: HashMap<String, Expr>,
121    ) -> Self {
122        self.column_defaults = column_defaults;
123        self
124    }
125
126    /// Specify an optional pre-known sort order(s). Must be `SortExpr`s.
127    ///
128    /// If the data is not sorted by this order, DataFusion may produce
129    /// incorrect results.
130    ///
131    /// DataFusion may take advantage of this ordering to omit sorts
132    /// or use more efficient algorithms.
133    ///
134    /// Note that multiple sort orders are supported, if some are known to be
135    /// equivalent,
136    pub fn with_sort_order(self, mut sort_order: Vec<Vec<SortExpr>>) -> Self {
137        std::mem::swap(self.sort_order.lock().as_mut(), &mut sort_order);
138        self
139    }
140
141    /// Create a mem table by reading from another data source
142    pub async fn load(
143        t: Arc<dyn TableProvider>,
144        output_partitions: Option<usize>,
145        state: &dyn Session,
146    ) -> Result<Self> {
147        let schema = t.schema();
148        let constraints = t.constraints().cloned().unwrap_or_default();
149
150        let exec = t.scan(state, None, &[], None).await?;
151        let data = collect_partitioned(exec, state.task_ctx()).await?;
152
153        // Optionally repartition the collected batches.
154        let data = if let Some(num_partitions) = output_partitions {
155            let source = DataSourceExec::new(Arc::new(MemorySourceConfig::try_new(
156                &data,
157                Arc::clone(&schema),
158                None,
159            )?));
160            let exec = RepartitionExec::try_new(
161                Arc::new(source),
162                Partitioning::RoundRobinBatch(num_partitions),
163            )?;
164            collect_partitioned(Arc::new(exec), state.task_ctx()).await?
165        } else {
166            data
167        };
168
169        MemTable::try_new(schema, data).map(|table| table.with_constraints(constraints))
170    }
171}
172
173#[async_trait]
174impl TableProvider for MemTable {
175    fn schema(&self) -> SchemaRef {
176        Arc::clone(&self.schema)
177    }
178
179    fn constraints(&self) -> Option<&Constraints> {
180        Some(&self.constraints)
181    }
182
183    fn table_type(&self) -> TableType {
184        TableType::Base
185    }
186
187    async fn scan(
188        &self,
189        state: &dyn Session,
190        projection: Option<&Vec<usize>>,
191        _filters: &[Expr],
192        _limit: Option<usize>,
193    ) -> Result<Arc<dyn ExecutionPlan>> {
194        let mut partitions = vec![];
195        for arc_inner_vec in self.batches.iter() {
196            let inner_vec = arc_inner_vec.read().await;
197            partitions.push(inner_vec.clone())
198        }
199
200        let mut source =
201            MemorySourceConfig::try_new(&partitions, self.schema(), projection.cloned())?;
202
203        let show_sizes = state.config_options().explain.show_sizes;
204        source = source.with_show_sizes(show_sizes);
205
206        // add sort information if present
207        let sort_order = self.sort_order.lock();
208        if !sort_order.is_empty() {
209            let df_schema = DFSchema::try_from(Arc::clone(&self.schema))?;
210
211            let eqp = state.execution_props();
212            let mut file_sort_order = vec![];
213            for sort_exprs in sort_order.iter() {
214                let physical_exprs = create_physical_sort_exprs(
215                    sort_exprs,
216                    &df_schema,
217                    eqp,
218                    &PhysicalPlanningContext::default(),
219                )?;
220                file_sort_order.extend(LexOrdering::new(physical_exprs));
221            }
222            source = source.try_with_sort_information(file_sort_order)?;
223        }
224
225        Ok(DataSourceExec::from_data_source(source))
226    }
227
228    /// Returns an ExecutionPlan that inserts the execution results of a given [`ExecutionPlan`] into this [`MemTable`].
229    ///
230    /// The [`ExecutionPlan`] must have the same schema as this [`MemTable`].
231    ///
232    /// # Arguments
233    ///
234    /// * `state` - The [`SessionState`] containing the context for executing the plan.
235    /// * `input` - The [`ExecutionPlan`] to execute and insert.
236    ///
237    /// # Returns
238    ///
239    /// * A plan that returns the number of rows written.
240    ///
241    /// [`SessionState`]: https://docs.rs/datafusion/latest/datafusion/execution/session_state/struct.SessionState.html
242    async fn insert_into(
243        &self,
244        _state: &dyn Session,
245        input: Arc<dyn ExecutionPlan>,
246        insert_op: InsertOp,
247    ) -> Result<Arc<dyn ExecutionPlan>> {
248        // If we are inserting into the table, any sort order may be messed up so reset it here
249        *self.sort_order.lock() = vec![];
250
251        // Create a physical plan from the logical plan.
252        // Check that the schema of the plan matches the schema of this table.
253        self.schema()
254            .logically_equivalent_names_and_types(&input.schema())?;
255
256        if insert_op != InsertOp::Append {
257            return not_impl_err!("{insert_op} not implemented for MemoryTable yet");
258        }
259        let sink = MemSink::try_new(self.batches.clone(), Arc::clone(&self.schema))?;
260        Ok(Arc::new(DataSinkExec::new(input, Arc::new(sink), None)))
261    }
262
263    fn get_column_default(&self, column: &str) -> Option<&Expr> {
264        self.column_defaults.get(column)
265    }
266
267    async fn delete_from(
268        &self,
269        state: &dyn Session,
270        filters: Vec<Expr>,
271    ) -> Result<Arc<dyn ExecutionPlan>> {
272        // Early exit if table has no partitions
273        if self.batches.is_empty() {
274            return Ok(Arc::new(DmlResultExec::new(0)));
275        }
276
277        *self.sort_order.lock() = vec![];
278
279        let mut total_deleted: u64 = 0;
280        let df_schema = DFSchema::try_from(Arc::clone(&self.schema))?;
281
282        for partition_data in &self.batches {
283            let mut partition = partition_data.write().await;
284            let mut new_batches = Vec::with_capacity(partition.len());
285
286            for batch in partition.iter() {
287                if batch.num_rows() == 0 {
288                    continue;
289                }
290
291                // Evaluate filters - None means "match all rows"
292                let filter_mask = evaluate_filters_to_mask(
293                    &filters,
294                    batch,
295                    &df_schema,
296                    state.execution_props(),
297                )?;
298
299                let (delete_count, keep_mask) = match filter_mask {
300                    Some(mask) => {
301                        // Count rows where mask is true (will be deleted)
302                        let count = mask.iter().filter(|v| v == &Some(true)).count();
303                        // Keep rows where predicate is false or NULL (SQL three-valued logic)
304                        let keep: BooleanArray =
305                            mask.iter().map(|v| Some(v != Some(true))).collect();
306                        (count, keep)
307                    }
308                    None => {
309                        // No filters = delete all rows
310                        (
311                            batch.num_rows(),
312                            BooleanArray::from(vec![false; batch.num_rows()]),
313                        )
314                    }
315                };
316
317                total_deleted += delete_count as u64;
318
319                let filtered_batch = filter_record_batch(batch, &keep_mask)?;
320                if filtered_batch.num_rows() > 0 {
321                    new_batches.push(filtered_batch);
322                }
323            }
324
325            *partition = new_batches;
326        }
327
328        Ok(Arc::new(DmlResultExec::new(total_deleted)))
329    }
330
331    async fn update(
332        &self,
333        state: &dyn Session,
334        assignments: Vec<(String, Expr)>,
335        filters: Vec<Expr>,
336    ) -> Result<Arc<dyn ExecutionPlan>> {
337        // Early exit if table has no partitions
338        if self.batches.is_empty() {
339            return Ok(Arc::new(DmlResultExec::new(0)));
340        }
341
342        // Validate column names upfront with clear error messages
343        let available_columns: Vec<&str> = self
344            .schema
345            .fields()
346            .iter()
347            .map(|f| f.name().as_str())
348            .collect();
349        for (column_name, _) in &assignments {
350            if self.schema.field_with_name(column_name).is_err() {
351                return plan_err!(
352                    "UPDATE failed: column '{}' does not exist. Available columns: {}",
353                    column_name,
354                    available_columns.join(", ")
355                );
356            }
357        }
358
359        let df_schema = DFSchema::try_from(Arc::clone(&self.schema))?;
360
361        // Create physical expressions for assignments upfront (outside batch loop)
362        let physical_assignments: HashMap<String, Arc<dyn PhysicalExpr>> = assignments
363            .iter()
364            .map(|(name, expr)| {
365                let physical_expr = create_physical_expr(
366                    expr,
367                    &df_schema,
368                    state.execution_props(),
369                    &PhysicalPlanningContext::default(),
370                )?;
371                Ok((name.clone(), physical_expr))
372            })
373            .collect::<Result<_>>()?;
374
375        *self.sort_order.lock() = vec![];
376
377        let mut total_updated: u64 = 0;
378
379        for partition_data in &self.batches {
380            let mut partition = partition_data.write().await;
381            let mut new_batches = Vec::with_capacity(partition.len());
382
383            for batch in partition.iter() {
384                if batch.num_rows() == 0 {
385                    continue;
386                }
387
388                // Evaluate filters - None means "match all rows"
389                let filter_mask = evaluate_filters_to_mask(
390                    &filters,
391                    batch,
392                    &df_schema,
393                    state.execution_props(),
394                )?;
395
396                let (update_count, update_mask) = match filter_mask {
397                    Some(mask) => {
398                        // Count rows where mask is true (will be updated)
399                        let count = mask.iter().filter(|v| v == &Some(true)).count();
400                        // Normalize mask: only true (not NULL) triggers update
401                        let normalized: BooleanArray =
402                            mask.iter().map(|v| Some(v == Some(true))).collect();
403                        (count, normalized)
404                    }
405                    None => {
406                        // No filters = update all rows
407                        (
408                            batch.num_rows(),
409                            BooleanArray::from(vec![true; batch.num_rows()]),
410                        )
411                    }
412                };
413
414                total_updated += update_count as u64;
415
416                if update_count == 0 {
417                    new_batches.push(batch.clone());
418                    continue;
419                }
420
421                let mut new_columns: Vec<ArrayRef> =
422                    Vec::with_capacity(batch.num_columns());
423
424                for field in self.schema.fields() {
425                    let column_name = field.name();
426                    let original_column =
427                        batch.column_by_name(column_name).ok_or_else(|| {
428                            datafusion_common::DataFusionError::Internal(format!(
429                                "Column '{column_name}' not found in batch"
430                            ))
431                        })?;
432
433                    let new_column = if let Some(physical_expr) =
434                        physical_assignments.get(column_name.as_str())
435                    {
436                        // Use evaluate_selection to only evaluate on matching rows.
437                        // This avoids errors (e.g., divide-by-zero) on rows that won't
438                        // be updated. The result is scattered back with nulls for
439                        // non-matching rows, which zip() will replace with originals.
440                        let new_values =
441                            physical_expr.evaluate_selection(batch, &update_mask)?;
442                        let new_array = new_values.into_array(batch.num_rows())?;
443
444                        // Convert to &dyn Array which implements Datum
445                        let new_arr: &dyn Array = new_array.as_ref();
446                        let orig_arr: &dyn Array = original_column.as_ref();
447                        zip(&update_mask, &new_arr, &orig_arr)?
448                    } else {
449                        Arc::clone(original_column)
450                    };
451
452                    new_columns.push(new_column);
453                }
454
455                let updated_batch =
456                    ArrowRecordBatch::try_new(Arc::clone(&self.schema), new_columns)?;
457                new_batches.push(updated_batch);
458            }
459
460            *partition = new_batches;
461        }
462
463        Ok(Arc::new(DmlResultExec::new(total_updated)))
464    }
465}
466
467/// Evaluate filter expressions against a batch and return a combined boolean mask.
468/// Returns None if filters is empty (meaning "match all rows").
469/// The returned mask has true for rows that match the filter predicates.
470fn evaluate_filters_to_mask(
471    filters: &[Expr],
472    batch: &RecordBatch,
473    df_schema: &DFSchema,
474    execution_props: &datafusion_expr::execution_props::ExecutionProps,
475) -> Result<Option<BooleanArray>> {
476    if filters.is_empty() {
477        return Ok(None);
478    }
479
480    let mut combined_mask: Option<BooleanArray> = None;
481
482    for filter_expr in filters {
483        let physical_expr = create_physical_expr(
484            filter_expr,
485            df_schema,
486            execution_props,
487            &PhysicalPlanningContext::default(),
488        )?;
489
490        let result = physical_expr.evaluate(batch)?;
491        let array = result.into_array(batch.num_rows())?;
492        let bool_array = array
493            .as_any()
494            .downcast_ref::<BooleanArray>()
495            .ok_or_else(|| {
496                datafusion_common::DataFusionError::Internal(
497                    "Filter did not evaluate to boolean".to_string(),
498                )
499            })?
500            .clone();
501
502        combined_mask = Some(match combined_mask {
503            Some(existing) => and(&existing, &bool_array)?,
504            None => bool_array,
505        });
506    }
507
508    Ok(combined_mask)
509}
510
511/// Returns a single row with the count of affected rows.
512#[derive(Debug)]
513struct DmlResultExec {
514    rows_affected: u64,
515    schema: SchemaRef,
516    properties: Arc<PlanProperties>,
517}
518
519impl DmlResultExec {
520    fn new(rows_affected: u64) -> Self {
521        let schema = Arc::new(Schema::new(vec![Field::new(
522            "count",
523            DataType::UInt64,
524            false,
525        )]));
526
527        let properties = PlanProperties::new(
528            datafusion_physical_expr::EquivalenceProperties::new(Arc::clone(&schema)),
529            Partitioning::UnknownPartitioning(1),
530            datafusion_physical_plan::execution_plan::EmissionType::Final,
531            datafusion_physical_plan::execution_plan::Boundedness::Bounded,
532        );
533
534        Self {
535            rows_affected,
536            schema,
537            properties: Arc::new(properties),
538        }
539    }
540}
541
542impl DisplayAs for DmlResultExec {
543    fn fmt_as(
544        &self,
545        t: DisplayFormatType,
546        f: &mut std::fmt::Formatter,
547    ) -> std::fmt::Result {
548        match t {
549            DisplayFormatType::Default
550            | DisplayFormatType::Verbose
551            | DisplayFormatType::TreeRender => {
552                write!(f, "DmlResultExec: rows_affected={}", self.rows_affected)
553            }
554        }
555    }
556}
557
558impl ExecutionPlan for DmlResultExec {
559    fn name(&self) -> &str {
560        "DmlResultExec"
561    }
562
563    fn schema(&self) -> SchemaRef {
564        Arc::clone(&self.schema)
565    }
566
567    fn properties(&self) -> &Arc<PlanProperties> {
568        &self.properties
569    }
570
571    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
572        vec![]
573    }
574
575    fn replace_children(
576        self: Arc<Self>,
577        _: Vec<Arc<dyn ExecutionPlan>>,
578        _: ReplaceChildrenOptions,
579    ) -> Result<Arc<dyn ExecutionPlan>> {
580        Ok(self)
581    }
582
583    fn with_new_children(
584        self: Arc<Self>,
585        children: Vec<Arc<dyn ExecutionPlan>>,
586    ) -> Result<Arc<dyn ExecutionPlan>> {
587        self.replace_children(
588            children,
589            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
590        )
591    }
592
593    fn execute(
594        &self,
595        _partition: usize,
596        _context: Arc<datafusion_execution::TaskContext>,
597    ) -> Result<datafusion_execution::SendableRecordBatchStream> {
598        // Create a single batch with the count
599        let count_array = UInt64Array::from(vec![self.rows_affected]);
600        let batch = ArrowRecordBatch::try_new(
601            Arc::clone(&self.schema),
602            vec![Arc::new(count_array) as ArrayRef],
603        )?;
604
605        // Create a stream that yields just this one batch
606        let stream = futures::stream::iter(vec![Ok(batch)]);
607        Ok(Box::pin(RecordBatchStreamAdapter::new(
608            Arc::clone(&self.schema),
609            stream,
610        )))
611    }
612
613    fn apply_expressions(
614        &self,
615        _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
616    ) -> Result<TreeNodeRecursion> {
617        Ok(TreeNodeRecursion::Continue)
618    }
619}