blob: b8448a5da6c42dd9b167712d82d874be9f8352e5 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt::{self, Debug, Display, Formatter};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use arrow::datatypes::{DataType, Field, Schema};
use datafusion_common::file_options::file_type::FileType;
use datafusion_common::{DFSchemaRef, TableReference};
use crate::{LogicalPlan, TableSource};
/// Operator that copies the contents of a database to file(s)
#[derive(Clone)]
pub struct CopyTo {
/// The relation that determines the tuples to write to the output file(s)
pub input: Arc<LogicalPlan>,
/// The location to write the file(s)
pub output_url: String,
/// Determines which, if any, columns should be used for hive-style partitioned writes
pub partition_by: Vec<String>,
/// File type trait
pub file_type: Arc<dyn FileType>,
/// SQL Options that can affect the formats
pub options: HashMap<String, String>,
/// The schema of the output (a single column "count")
pub output_schema: DFSchemaRef,
}
impl Debug for CopyTo {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("CopyTo")
.field("input", &self.input)
.field("output_url", &self.output_url)
.field("partition_by", &self.partition_by)
.field("file_type", &"...")
.field("options", &self.options)
.field("output_schema", &self.output_schema)
.finish_non_exhaustive()
}
}
// Implement PartialEq manually
impl PartialEq for CopyTo {
fn eq(&self, other: &Self) -> bool {
self.input == other.input && self.output_url == other.output_url
}
}
// Implement Eq (no need for additional logic over PartialEq)
impl Eq for CopyTo {}
// Manual implementation needed because of `file_type` and `options` fields.
// Comparison excludes these field.
impl PartialOrd for CopyTo {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match self.input.partial_cmp(&other.input) {
Some(Ordering::Equal) => match self.output_url.partial_cmp(&other.output_url)
{
Some(Ordering::Equal) => {
self.partition_by.partial_cmp(&other.partition_by)
}
cmp => cmp,
},
cmp => cmp,
}
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
.filter(|cmp| *cmp != Ordering::Equal || self == other)
}
}
// Implement Hash manually
impl Hash for CopyTo {
fn hash<H: Hasher>(&self, state: &mut H) {
self.input.hash(state);
self.output_url.hash(state);
}
}
impl CopyTo {
pub fn new(
input: Arc<LogicalPlan>,
output_url: String,
partition_by: Vec<String>,
file_type: Arc<dyn FileType>,
options: HashMap<String, String>,
) -> Self {
Self {
input,
output_url,
partition_by,
file_type,
options,
// The output schema is always a single column "count" with the number of rows copied
output_schema: make_count_schema(),
}
}
}
/// Modifies the content of a database
///
/// This operator is used to perform DML operations such as INSERT, DELETE,
/// UPDATE, and CTAS (CREATE TABLE AS SELECT).
///
/// * `INSERT` - Appends new rows to the existing table. Calls
/// [`TableProvider::insert_into`]
///
/// * `DELETE` - Removes rows from the table. Currently NOT supported by the
/// [`TableProvider`] trait or builtin sources.
///
/// * `UPDATE` - Modifies existing rows in the table. Currently NOT supported by
/// the [`TableProvider`] trait or builtin sources.
///
/// * `CREATE TABLE AS SELECT` - Creates a new table and populates it with data
/// from a query. This is similar to the `INSERT` operation, but it creates a new
/// table instead of modifying an existing one.
///
/// Note that the structure is adapted from substrait WriteRel)
///
/// [`TableProvider`]: https://docs.rs/datafusion/latest/datafusion/datasource/trait.TableProvider.html
/// [`TableProvider::insert_into`]: https://docs.rs/datafusion/latest/datafusion/datasource/trait.TableProvider.html#method.insert_into
#[derive(Clone)]
pub struct DmlStatement {
/// The table name
pub table_name: TableReference,
/// this is target table to insert into
pub target: Arc<dyn TableSource>,
/// The type of operation to perform
pub op: WriteOp,
/// The relation that determines the tuples to add/remove/modify the schema must match with table_schema
pub input: Arc<LogicalPlan>,
/// The schema of the output relation
pub output_schema: DFSchemaRef,
}
impl Eq for DmlStatement {}
impl Hash for DmlStatement {
fn hash<H: Hasher>(&self, state: &mut H) {
self.table_name.hash(state);
self.target.schema().hash(state);
self.op.hash(state);
self.input.hash(state);
self.output_schema.hash(state);
}
}
impl PartialEq for DmlStatement {
fn eq(&self, other: &Self) -> bool {
self.table_name == other.table_name
&& self.target.schema() == other.target.schema()
&& self.op == other.op
&& self.input == other.input
&& self.output_schema == other.output_schema
}
}
impl Debug for DmlStatement {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("DmlStatement")
.field("table_name", &self.table_name)
.field("target", &"...")
.field("target_schema", &self.target.schema())
.field("op", &self.op)
.field("input", &self.input)
.field("output_schema", &self.output_schema)
.finish()
}
}
impl DmlStatement {
/// Creates a new DML statement with the output schema set to a single `count` column.
pub fn new(
table_name: TableReference,
target: Arc<dyn TableSource>,
op: WriteOp,
input: Arc<LogicalPlan>,
) -> Self {
Self {
table_name,
target,
op,
input,
// The output schema is always a single column with the number of rows affected
output_schema: make_count_schema(),
}
}
/// Return a descriptive name of this [`DmlStatement`]
pub fn name(&self) -> &str {
self.op.name()
}
}
// Manual implementation needed because of `table_schema` and `output_schema` fields.
// Comparison excludes these fields.
impl PartialOrd for DmlStatement {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match self.table_name.partial_cmp(&other.table_name) {
Some(Ordering::Equal) => match self.op.partial_cmp(&other.op) {
Some(Ordering::Equal) => self.input.partial_cmp(&other.input),
cmp => cmp,
},
cmp => cmp,
}
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
.filter(|cmp| *cmp != Ordering::Equal || self == other)
}
}
/// The type of DML operation to perform.
///
/// See [`DmlStatement`] for more details.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash)]
pub enum WriteOp {
/// `INSERT INTO` operation
Insert(InsertOp),
/// `DELETE` operation
Delete,
/// `UPDATE` operation
Update,
/// `CREATE TABLE AS SELECT` operation
Ctas,
}
impl WriteOp {
/// Return a descriptive name of this [`WriteOp`]
pub fn name(&self) -> &str {
match self {
WriteOp::Insert(insert) => insert.name(),
WriteOp::Delete => "Delete",
WriteOp::Update => "Update",
WriteOp::Ctas => "Ctas",
}
}
}
impl Display for WriteOp {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash)]
pub enum InsertOp {
/// Appends new rows to the existing table without modifying any
/// existing rows. This corresponds to the SQL `INSERT INTO` query.
Append,
/// Overwrites all existing rows in the table with the new rows.
/// This corresponds to the SQL `INSERT OVERWRITE` query.
Overwrite,
/// If any existing rows collides with the inserted rows (typically based
/// on a unique key or primary key), those existing rows are replaced.
/// This corresponds to the SQL `REPLACE INTO` query and its equivalents.
Replace,
}
impl InsertOp {
/// Return a descriptive name of this [`InsertOp`]
pub fn name(&self) -> &str {
match self {
InsertOp::Append => "Insert Into",
InsertOp::Overwrite => "Insert Overwrite",
InsertOp::Replace => "Replace Into",
}
}
}
impl Display for InsertOp {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
fn make_count_schema() -> DFSchemaRef {
Arc::new(
Schema::new(vec![Field::new("count", DataType::UInt64, false)])
.try_into()
.unwrap(),
)
}