Skip to content

Commit e4f2a13

Browse files
SQUASH – review comments
docs: move row-level DML contracts into TableProvider API docs Keep the library guide introductory and split the SQL reference changes into a separate PR.
1 parent 989cb7c commit e4f2a13

3 files changed

Lines changed: 75 additions & 243 deletions

File tree

‎datafusion/session/src/table.rs‎

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,38 @@ pub trait TableProvider: Any + Debug + Sync + Send {
359359

360360
/// Delete rows matching the filter predicates.
361361
///
362-
/// Returns an [`ExecutionPlan`] producing a single row with `count` (UInt64).
363-
/// Empty `filters` deletes all rows.
362+
/// Returns an [`ExecutionPlan`] producing one row in a single non-null
363+
/// `UInt64` column named `count`, containing the number of deleted rows.
364+
/// The default implementation returns a "not implemented" error.
365+
///
366+
/// # Filters
367+
///
368+
/// `filters` contains logical [`Expr`] predicates on the target table.
369+
/// The planner collects them from filters and pushed-down table scan
370+
/// predicates, splits `AND` conjunctions, removes table qualifiers, and
371+
/// deduplicates them. For example, `t.id = 1 AND t.value > 15` becomes
372+
/// separate `id = 1` and `value > 15` expressions.
373+
///
374+
/// Delete a row only when every predicate evaluates to true. SQL
375+
/// three-valued logic applies: a false or `NULL` predicate leaves the row
376+
/// unchanged. Empty `filters` deletes all rows, as for `DELETE FROM t`
377+
/// without a `WHERE` clause.
378+
///
379+
/// # Execution
380+
///
381+
/// This method is called during physical planning, including for `EXPLAIN`.
382+
/// Perform mutations when the returned plan executes, rather than while
383+
/// constructing it, so planning does not change the table.
384+
///
385+
/// # Limitations
386+
///
387+
/// The method receives no row limit ([#24998]). Subqueries in DML
388+
/// expressions are not fully supported ([#24654]); in particular, a
389+
/// subquery rewritten into a join can cause predicates to be lost before
390+
/// this method is called.
391+
///
392+
/// [#24998]: https://github.com/apache/datafusion/issues/24998
393+
/// [#24654]: https://github.com/apache/datafusion/issues/24654
364394
// Hand-written `#[async_trait]` expansion to reduce compile time. See
365395
// <https://github.com/apache/datafusion/issues/13814#issuecomment-5292709677>
366396
fn delete_from<'life0, 'life1, 'async_trait>(
@@ -381,8 +411,43 @@ pub trait TableProvider: Any + Debug + Sync + Send {
381411

382412
/// Update rows matching the filter predicates.
383413
///
384-
/// Returns an [`ExecutionPlan`] producing a single row with `count` (UInt64).
385-
/// Empty `filters` updates all rows.
414+
/// Returns an [`ExecutionPlan`] producing one row in a single non-null
415+
/// `UInt64` column named `count`, containing the number of affected rows.
416+
/// The default implementation returns a "not implemented" error.
417+
///
418+
/// # Filters
419+
///
420+
/// `filters` follows the same conventions as [`Self::delete_from`]:
421+
/// predicates are combined with `AND`, table qualifiers are removed, and
422+
/// only rows for which every predicate is true are updated. A false or
423+
/// `NULL` predicate leaves the row unchanged. Empty `filters` updates all
424+
/// rows.
425+
///
426+
/// # Assignments
427+
///
428+
/// `assignments` contains `(column_name, Expr)` pairs from the `SET`
429+
/// clause. The planner removes identity assignments and strips table
430+
/// qualifiers from the expressions. Leave columns without an assignment
431+
/// unchanged.
432+
///
433+
/// Evaluate every assignment against the row values from before the
434+
/// statement, and only for matching rows. For example, `SET a = b, b = a`
435+
/// exchanges the two values; an expression such as `100 / divisor` must
436+
/// not be evaluated on rows excluded by the filters.
437+
///
438+
/// # Execution
439+
///
440+
/// Like [`Self::delete_from`], this method is called during physical
441+
/// planning, including for `EXPLAIN`. Perform mutations when the returned
442+
/// plan executes so planning does not change the table.
443+
///
444+
/// # Limitations
445+
///
446+
/// Subqueries have the same limitations as in [`Self::delete_from`]
447+
/// ([#24654]). `UPDATE ... FROM` is not supported ([#19950]).
448+
///
449+
/// [#24654]: https://github.com/apache/datafusion/issues/24654
450+
/// [#19950]: https://github.com/apache/datafusion/issues/19950
386451
// Hand-written `#[async_trait]` expansion to reduce compile time. See
387452
// <https://github.com/apache/datafusion/issues/13814#issuecomment-5292709677>
388453
fn update<'life0, 'life1, 'async_trait>(

‎docs/source/library-user-guide/custom-table-providers.md‎

Lines changed: 6 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -794,138 +794,16 @@ happens later, in the stream produced by `execute()`.
794794

795795
## Row-Level DML: DELETE and UPDATE
796796

797-
`TableProvider` has two optional hooks for row-level Data Manipulation Language (DML) statements:
797+
A custom table provider can support `DELETE` and `UPDATE` by implementing the optional [`TableProvider::delete_from()`] and [`TableProvider::update()`] methods. Their default implementations return a "not implemented" error.
798798

799-
- `delete_from(state, filters)` serves `DELETE FROM t [WHERE ...]`.
800-
- `update(state, assignments, filters)` serves `UPDATE t SET ... [WHERE ...]`.
799+
Each method builds an [ExecutionPlan] that changes the matching rows and returns the number of affected rows in a `count` column. Your provider decides how to apply and persist the changes. Perform the changes when the returned plan executes: these methods run during physical planning, including for `EXPLAIN`.
801800

802-
Both hooks have a default implementation that returns a "not implemented" error. A provider that does not override them keeps its previous read-only behaviour, and DataFusion reports the statement as unsupported for that table.
801+
The method documentation describes the filters, assignments, SQL semantics, and result schema that an implementation must support. [MemTable] provides an implementation of both methods over in-memory batches.
803802

804-
The physical planner calls the hook instead of building a plan of its own. Your provider therefore owns the whole row change: which rows change, how to make the change durable, and how many rows the statement affected.
803+
For SQL syntax and examples, see the [DML section](../user-guide/sql/dml.md) of the user guide.
805804

806-
### What the Planner Passes to the Hooks
807-
808-
`filters` holds the `WHERE` predicates as logical `Expr` values, after three transformations:
809-
810-
- The planner splits `AND` conjunctions into separate elements.
811-
- The planner strips table qualifiers, so `t.id = 1` arrives as `id = 1` and matches your schema.
812-
- The planner keeps only the predicates on the target table, and collects them both from `Filter` nodes and from the pushed-down filters of the target `TableScan`.
813-
814-
An empty `filters` vector means the statement has no `WHERE` clause. Then the statement applies to every row.
815-
816-
`assignments` holds the `SET` clause as `(column_name, Expr)` pairs. The planner removes identity assignments, so the vector contains only the columns that the statement changes.
817-
818-
### What the Hooks Must Return
819-
820-
Each hook returns an [ExecutionPlan] that produces one row in a single non-null `UInt64` column named `count`. The value is the number of affected rows. `INSERT` uses the same convention, which is why `datafusion-cli` prints the same shape of result for all three statements:
821-
822-
```text
823-
+-------+
824-
| count |
825-
+-------+
826-
| 2 |
827-
+-------+
828-
```
829-
830-
To evaluate the filters and the assignments, convert each `Expr` with `create_physical_expr()`, then pass your table schema and `state.execution_props()`. Reject an unknown column with a plan error rather than an internal error.
831-
832-
Two semantic rules keep your provider consistent with the rest of SQL:
833-
834-
- Apply SQL three-valued logic. Change a row only if the predicate is true for that row. A predicate that evaluates to `NULL` must leave the row alone.
835-
- Evaluate every assignment against the values from before the statement. `SET a = b, b = a` then exchanges the two values.
836-
837-
The following example shows the shape of both implementations. The row-level work is in the two private methods. The example counts the rows in the hook, as [MemTable] does; see [When the Work Happens](#when-the-work-happens) for the alternative:
838-
839-
```rust
840-
# use std::sync::Arc;
841-
# use arrow::array::{ArrayRef, RecordBatch, UInt64Array};
842-
# use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
843-
# use datafusion::catalog::{Session, TableProvider};
844-
# use datafusion::common::Result;
845-
# use datafusion::datasource::TableType;
846-
# use datafusion::datasource::memory::MemorySourceConfig;
847-
# use datafusion::logical_expr::Expr;
848-
# use datafusion::physical_plan::ExecutionPlan;
849-
#
850-
# #[derive(Debug)]
851-
# struct MyMutableTable {
852-
# schema: SchemaRef,
853-
# }
854-
#
855-
# impl MyMutableTable {
856-
# fn remove_rows(&self, _state: &dyn Session, _filters: &[Expr]) -> Result<u64> {
857-
# Ok(0)
858-
# }
859-
#
860-
# fn change_rows(
861-
# &self,
862-
# _state: &dyn Session,
863-
# _assignments: &[(String, Expr)],
864-
# _filters: &[Expr],
865-
# ) -> Result<u64> {
866-
# Ok(0)
867-
# }
868-
# }
869-
#
870-
/// Build the single-row `count` plan that both hooks must return.
871-
fn count_plan(rows_affected: u64) -> Result<Arc<dyn ExecutionPlan>> {
872-
let schema = Arc::new(Schema::new(vec![Field::new(
873-
"count",
874-
DataType::UInt64,
875-
false,
876-
)]));
877-
let count = Arc::new(UInt64Array::from(vec![rows_affected])) as ArrayRef;
878-
let batch = RecordBatch::try_new(Arc::clone(&schema), vec![count])?;
879-
Ok(MemorySourceConfig::try_new_exec(&[vec![batch]], schema, None)?)
880-
}
881-
882-
#[async_trait::async_trait]
883-
impl TableProvider for MyMutableTable {
884-
# fn schema(&self) -> SchemaRef { Arc::clone(&self.schema) }
885-
# fn table_type(&self) -> TableType { TableType::Base }
886-
# async fn scan(&self, _: &dyn Session, _: Option<&[usize]>, _: &[Expr], _: Option<usize>) -> Result<Arc<dyn ExecutionPlan>> { todo!() }
887-
async fn delete_from(
888-
&self,
889-
state: &dyn Session,
890-
filters: Vec<Expr>,
891-
) -> Result<Arc<dyn ExecutionPlan>> {
892-
// An empty `filters` means `DELETE FROM t` with no `WHERE` clause.
893-
let rows_affected = self.remove_rows(state, &filters)?;
894-
count_plan(rows_affected)
895-
}
896-
897-
async fn update(
898-
&self,
899-
state: &dyn Session,
900-
assignments: Vec<(String, Expr)>,
901-
filters: Vec<Expr>,
902-
) -> Result<Arc<dyn ExecutionPlan>> {
903-
let rows_affected = self.change_rows(state, &assignments, &filters)?;
904-
count_plan(rows_affected)
905-
}
906-
}
907-
```
908-
909-
### What the Hooks Do Not Receive
910-
911-
The planner drops some clauses before it calls a hook, so do not expect them:
912-
913-
- A `LIMIT` clause on a `DELETE` has no effect. Your hook sees only the filters. This limitation is tracked at [issue #24998](https://github.com/apache/datafusion/issues/24998).
914-
- A scalar subquery in a `WHERE` clause or in a `SET` clause fails during physical planning, before the hook runs. This limitation is tracked at [issue #24654](https://github.com/apache/datafusion/issues/24654).
915-
- An `IN` or an `EXISTS` subquery reaches your hook with an empty `filters` vector, because the optimizer rewrites the subquery into a join. Your hook then changes every row, which is the wrong answer. DataFusion does not yet protect a provider against this case. This limitation is tracked at [issue #24654](https://github.com/apache/datafusion/issues/24654).
916-
- `UPDATE ... FROM`, which reads new values from a second table, returns a "not implemented" error. This limitation is tracked at [issue #19950](https://github.com/apache/datafusion/issues/19950).
917-
918-
### When the Work Happens
919-
920-
The hooks run during physical planning, like `scan()`. A hook that changes rows before it returns its plan therefore changes them during planning, and `EXPLAIN DELETE` or `EXPLAIN UPDATE` also changes them. [MemTable] works this way. This limitation is tracked at [issue #24656](https://github.com/apache/datafusion/issues/24656).
921-
922-
For a provider that writes to durable storage, do the work in the `execute()` method of the plan that you return instead. The hook then stays lightweight, and `EXPLAIN` shows the plan without a side effect.
923-
924-
### Reference Implementation
925-
926-
[MemTable] implements both hooks over its in-memory batches, and is the best code to read next. It shows how to build the boolean mask for the filters, how to keep the rows where the predicate is false or `NULL`, and how to evaluate an assignment only on the matching rows. That last detail matters: it stops an expression such as `100 / divisor` from failing on rows that the `WHERE` clause excludes.
927-
928-
The user-facing behaviour of the two statements is in the [DML section](../user-guide/sql/dml.md) of the user guide.
805+
[`tableprovider::delete_from()`]: https://docs.rs/datafusion/latest/datafusion/catalog/trait.TableProvider.html#method.delete_from
806+
[`tableprovider::update()`]: https://docs.rs/datafusion/latest/datafusion/catalog/trait.TableProvider.html#method.update
929807

930808
## Putting It All Together
931809

‎docs/source/user-guide/sql/dml.md‎

Lines changed: 0 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -136,114 +136,3 @@ INSERT INTO <i><b>table_name</i></b> { VALUES ( <i><b>expression</i></b> [, ...]
136136
| 2 |
137137
+-------+
138138
```
139-
140-
## DELETE
141-
142-
Removes rows from a table.
143-
144-
<pre>
145-
DELETE FROM <i><b>table_name</b></i> [ WHERE <i><b>condition</b></i> ]
146-
</pre>
147-
148-
`DELETE` returns the number of removed rows in a column named `count`.
149-
150-
If you omit the `WHERE` clause, DataFusion removes all rows.
151-
152-
DataFusion removes a row only if the condition is true for that row. SQL three-valued logic applies: if the condition evaluates to `NULL`, the row remains. For example, `WHERE value > 15` keeps a row with a `NULL` value, because `NULL > 15` is `NULL`.
153-
154-
Not all tables support `DELETE`. See [Table support for DELETE and UPDATE](#table-support-for-delete-and-update).
155-
156-
### Examples
157-
158-
Remove the rows that match a condition:
159-
160-
```sql
161-
> DELETE FROM target_table WHERE id > 1;
162-
+-------+
163-
| count |
164-
+-------+
165-
| 2 |
166-
+-------+
167-
```
168-
169-
Remove all rows:
170-
171-
```sql
172-
> DELETE FROM target_table;
173-
+-------+
174-
| count |
175-
+-------+
176-
| 3 |
177-
+-------+
178-
```
179-
180-
## UPDATE
181-
182-
Changes the values of existing rows.
183-
184-
<pre>
185-
UPDATE <i><b>table_name</b></i> SET <i><b>column</b></i> = <i><b>expression</b></i> [, ...] [ WHERE <i><b>condition</b></i> ]
186-
</pre>
187-
188-
`UPDATE` returns the number of affected rows in a column named `count`.
189-
190-
If you omit the `WHERE` clause, DataFusion changes all rows. The three-valued logic of `DELETE` also applies here.
191-
192-
Each assignment expression reads the row values from before the statement. `SET a = b, b = a` therefore exchanges the two values.
193-
194-
Not all tables support `UPDATE`. See [Table support for DELETE and UPDATE](#table-support-for-delete-and-update).
195-
196-
### Examples
197-
198-
Set one column in the rows that match a condition:
199-
200-
```sql
201-
> UPDATE target_table SET name = 'Baz' WHERE id = 2;
202-
+-------+
203-
| count |
204-
+-------+
205-
| 1 |
206-
+-------+
207-
```
208-
209-
Set two columns, one from an expression:
210-
211-
```sql
212-
> UPDATE target_table SET value = value * 2, name = 'Doubled' WHERE id < 3;
213-
+-------+
214-
| count |
215-
+-------+
216-
| 2 |
217-
+-------+
218-
```
219-
220-
## Table support for DELETE and UPDATE
221-
222-
The table provider does the work for `DELETE` and `UPDATE`. Support is therefore a property of each table:
223-
224-
- `CREATE TABLE` makes an in-memory table. In-memory tables support both statements.
225-
- `CREATE EXTERNAL TABLE` makes a file-based table. File-based tables support neither statement.
226-
- Views support neither statement.
227-
- A custom table provider supports a statement only if it implements the matching hook. See [Custom Table Provider](../../library-user-guide/custom-table-providers.md#row-level-dml-delete-and-update).
228-
229-
A table that gives no support returns an error:
230-
231-
```text
232-
DELETE operation on table 'my_external_table'
233-
caused by
234-
This feature is not implemented: DELETE not supported for Base table
235-
```
236-
237-
### Limitations
238-
239-
:::{warning}
240-
Do not use a subquery in the condition of a `DELETE` or an `UPDATE`. A scalar subquery, such as `WHERE id = (SELECT max(id) FROM other)`, returns an error. An `IN` or an `EXISTS` subquery is worse: the statement applies to **all** rows of the table. The optimizer rewrites the subquery into a join, and the condition then no longer reaches the table provider. This limitation is tracked at [issue #24654](https://github.com/apache/datafusion/issues/24654).
241-
:::
242-
243-
:::{warning}
244-
`EXPLAIN` executes a `DELETE` or an `UPDATE` on an in-memory table. The provider changes the rows while DataFusion plans the statement. Use a copy of the table if you want to read the plan only. This limitation is tracked at [issue #24656](https://github.com/apache/datafusion/issues/24656).
245-
:::
246-
247-
DataFusion ignores a `LIMIT` clause in a `DELETE` statement. The statement removes all rows that match the condition. This limitation is tracked at [issue #24998](https://github.com/apache/datafusion/issues/24998).
248-
249-
`UPDATE ... FROM`, which reads the new values from a second table, returns a "not implemented" error. This limitation is tracked at [issue #19950](https://github.com/apache/datafusion/issues/19950).

0 commit comments

Comments
 (0)