Skip to main content

datafusion_python/common/
schema.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
18use std::borrow::Cow;
19use std::fmt::{self, Display, Formatter};
20use std::sync::Arc;
21
22use arrow::datatypes::Schema;
23use arrow::pyarrow::PyArrowType;
24use datafusion::arrow::datatypes::SchemaRef;
25use datafusion::common::Constraints;
26use datafusion::datasource::TableType;
27use datafusion::logical_expr::utils::split_conjunction;
28use datafusion::logical_expr::{Expr, TableProviderFilterPushDown, TableSource};
29use parking_lot::RwLock;
30use pyo3::prelude::*;
31
32use super::data_type::DataTypeMap;
33use super::function::SqlFunction;
34use crate::sql::logical::PyLogicalPlan;
35
36#[pyclass(
37    from_py_object,
38    name = "SqlSchema",
39    module = "datafusion.common",
40    subclass,
41    frozen
42)]
43#[derive(Debug, Clone)]
44pub struct SqlSchema {
45    name: Arc<RwLock<String>>,
46    tables: Arc<RwLock<Vec<SqlTable>>>,
47    views: Arc<RwLock<Vec<SqlView>>>,
48    functions: Arc<RwLock<Vec<SqlFunction>>>,
49}
50
51#[pyclass(
52    from_py_object,
53    name = "SqlTable",
54    module = "datafusion.common",
55    subclass
56)]
57#[derive(Debug, Clone)]
58pub struct SqlTable {
59    #[pyo3(get, set)]
60    pub name: String,
61    #[pyo3(get, set)]
62    pub columns: Vec<(String, DataTypeMap)>,
63    #[pyo3(get, set)]
64    pub primary_key: Option<String>,
65    #[pyo3(get, set)]
66    pub foreign_keys: Vec<String>,
67    #[pyo3(get, set)]
68    pub indexes: Vec<String>,
69    #[pyo3(get, set)]
70    pub constraints: Vec<String>,
71    #[pyo3(get, set)]
72    pub statistics: SqlStatistics,
73    #[pyo3(get, set)]
74    pub filepaths: Option<Vec<String>>,
75}
76
77#[pymethods]
78impl SqlTable {
79    #[new]
80    #[pyo3(signature = (table_name, columns, row_count, filepaths=None))]
81    pub fn new(
82        table_name: String,
83        columns: Vec<(String, DataTypeMap)>,
84        row_count: f64,
85        filepaths: Option<Vec<String>>,
86    ) -> Self {
87        Self {
88            name: table_name,
89            columns,
90            primary_key: None,
91            foreign_keys: Vec::new(),
92            indexes: Vec::new(),
93            constraints: Vec::new(),
94            statistics: SqlStatistics::new(row_count),
95            filepaths,
96        }
97    }
98}
99
100#[pyclass(
101    from_py_object,
102    name = "SqlView",
103    module = "datafusion.common",
104    subclass
105)]
106#[derive(Debug, Clone)]
107pub struct SqlView {
108    #[pyo3(get, set)]
109    pub name: String,
110    #[pyo3(get, set)]
111    pub definition: String, // SQL code that defines the view
112}
113
114#[pymethods]
115impl SqlSchema {
116    #[new]
117    pub fn new(schema_name: &str) -> Self {
118        Self {
119            name: Arc::new(RwLock::new(schema_name.to_owned())),
120            tables: Arc::new(RwLock::new(Vec::new())),
121            views: Arc::new(RwLock::new(Vec::new())),
122            functions: Arc::new(RwLock::new(Vec::new())),
123        }
124    }
125
126    #[getter]
127    fn name(&self) -> PyResult<String> {
128        Ok(self.name.read().clone())
129    }
130
131    #[setter]
132    fn set_name(&self, value: String) -> PyResult<()> {
133        *self.name.write() = value;
134        Ok(())
135    }
136
137    #[getter]
138    fn tables(&self) -> PyResult<Vec<SqlTable>> {
139        Ok(self.tables.read().clone())
140    }
141
142    #[setter]
143    fn set_tables(&self, tables: Vec<SqlTable>) -> PyResult<()> {
144        *self.tables.write() = tables;
145        Ok(())
146    }
147
148    #[getter]
149    fn views(&self) -> PyResult<Vec<SqlView>> {
150        Ok(self.views.read().clone())
151    }
152
153    #[setter]
154    fn set_views(&self, views: Vec<SqlView>) -> PyResult<()> {
155        *self.views.write() = views;
156        Ok(())
157    }
158
159    #[getter]
160    fn functions(&self) -> PyResult<Vec<SqlFunction>> {
161        Ok(self.functions.read().clone())
162    }
163
164    #[setter]
165    fn set_functions(&self, functions: Vec<SqlFunction>) -> PyResult<()> {
166        *self.functions.write() = functions;
167        Ok(())
168    }
169
170    pub fn table_by_name(&self, table_name: &str) -> Option<SqlTable> {
171        let tables = self.tables.read();
172        tables.iter().find(|tbl| tbl.name.eq(table_name)).cloned()
173    }
174
175    pub fn add_table(&self, table: SqlTable) {
176        let mut tables = self.tables.write();
177        tables.push(table);
178    }
179
180    pub fn drop_table(&self, table_name: String) {
181        let mut tables = self.tables.write();
182        tables.retain(|x| !x.name.eq(&table_name));
183    }
184}
185
186/// SqlTable wrapper that is compatible with DataFusion logical query plans
187pub struct SqlTableSource {
188    schema: SchemaRef,
189    statistics: Option<SqlStatistics>,
190    filepaths: Option<Vec<String>>,
191}
192
193impl SqlTableSource {
194    /// Initialize a new `EmptyTable` from a schema
195    pub fn new(
196        schema: SchemaRef,
197        statistics: Option<SqlStatistics>,
198        filepaths: Option<Vec<String>>,
199    ) -> Self {
200        Self {
201            schema,
202            statistics,
203            filepaths,
204        }
205    }
206
207    /// Access optional statistics associated with this table source
208    pub fn statistics(&self) -> Option<&SqlStatistics> {
209        self.statistics.as_ref()
210    }
211
212    /// Access optional filepath associated with this table source
213    #[allow(dead_code)]
214    pub fn filepaths(&self) -> Option<&Vec<String>> {
215        self.filepaths.as_ref()
216    }
217}
218
219/// Implement TableSource, used in the logical query plan and in logical query optimizations
220impl TableSource for SqlTableSource {
221    fn schema(&self) -> SchemaRef {
222        self.schema.clone()
223    }
224
225    fn table_type(&self) -> datafusion::logical_expr::TableType {
226        datafusion::logical_expr::TableType::Base
227    }
228
229    fn supports_filters_pushdown(
230        &self,
231        filters: &[&Expr],
232    ) -> datafusion::common::Result<Vec<TableProviderFilterPushDown>> {
233        filters
234            .iter()
235            .map(|f| {
236                let filters = split_conjunction(f);
237                if filters.iter().all(|f| is_supported_push_down_expr(f)) {
238                    // Push down filters to the tablescan operation if all are supported
239                    Ok(TableProviderFilterPushDown::Exact)
240                } else if filters.iter().any(|f| is_supported_push_down_expr(f)) {
241                    // Partially apply the filter in the TableScan but retain
242                    // the Filter operator in the plan as well
243                    Ok(TableProviderFilterPushDown::Inexact)
244                } else {
245                    Ok(TableProviderFilterPushDown::Unsupported)
246                }
247            })
248            .collect()
249    }
250
251    fn get_logical_plan(&self) -> Option<Cow<'_, datafusion::logical_expr::LogicalPlan>> {
252        None
253    }
254}
255
256fn is_supported_push_down_expr(_expr: &Expr) -> bool {
257    // For now we support all kinds of expr's at this level
258    true
259}
260
261#[pyclass(
262    from_py_object,
263    frozen,
264    name = "SqlStatistics",
265    module = "datafusion.common",
266    subclass
267)]
268#[derive(Debug, Clone)]
269pub struct SqlStatistics {
270    row_count: f64,
271}
272
273#[pymethods]
274impl SqlStatistics {
275    #[new]
276    pub fn new(row_count: f64) -> Self {
277        Self { row_count }
278    }
279
280    #[pyo3(name = "getRowCount")]
281    pub fn get_row_count(&self) -> f64 {
282        self.row_count
283    }
284}
285
286#[pyclass(
287    from_py_object,
288    frozen,
289    name = "Constraints",
290    module = "datafusion.expr",
291    subclass
292)]
293#[derive(Clone)]
294pub struct PyConstraints {
295    pub constraints: Constraints,
296}
297
298impl From<PyConstraints> for Constraints {
299    fn from(constraints: PyConstraints) -> Self {
300        constraints.constraints
301    }
302}
303
304impl From<Constraints> for PyConstraints {
305    fn from(constraints: Constraints) -> Self {
306        PyConstraints { constraints }
307    }
308}
309
310impl Display for PyConstraints {
311    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
312        write!(f, "Constraints: {:?}", self.constraints)
313    }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
317#[pyclass(
318    from_py_object,
319    frozen,
320    eq,
321    eq_int,
322    name = "TableType",
323    module = "datafusion.common"
324)]
325pub enum PyTableType {
326    Base,
327    View,
328    Temporary,
329}
330
331impl From<PyTableType> for datafusion::logical_expr::TableType {
332    fn from(table_type: PyTableType) -> Self {
333        match table_type {
334            PyTableType::Base => datafusion::logical_expr::TableType::Base,
335            PyTableType::View => datafusion::logical_expr::TableType::View,
336            PyTableType::Temporary => datafusion::logical_expr::TableType::Temporary,
337        }
338    }
339}
340
341impl From<TableType> for PyTableType {
342    fn from(table_type: TableType) -> Self {
343        match table_type {
344            datafusion::logical_expr::TableType::Base => PyTableType::Base,
345            datafusion::logical_expr::TableType::View => PyTableType::View,
346            datafusion::logical_expr::TableType::Temporary => PyTableType::Temporary,
347        }
348    }
349}
350
351#[pyclass(
352    from_py_object,
353    frozen,
354    name = "TableSource",
355    module = "datafusion.common",
356    subclass
357)]
358#[derive(Clone)]
359pub struct PyTableSource {
360    pub table_source: Arc<dyn TableSource>,
361}
362
363#[pymethods]
364impl PyTableSource {
365    pub fn schema(&self) -> PyArrowType<Schema> {
366        (*self.table_source.schema()).clone().into()
367    }
368
369    pub fn constraints(&self) -> Option<PyConstraints> {
370        self.table_source.constraints().map(|c| PyConstraints {
371            constraints: c.clone(),
372        })
373    }
374
375    pub fn table_type(&self) -> PyTableType {
376        self.table_source.table_type().into()
377    }
378
379    pub fn get_logical_plan(&self) -> Option<PyLogicalPlan> {
380        self.table_source
381            .get_logical_plan()
382            .map(|plan| PyLogicalPlan::new(plan.into_owned()))
383    }
384}