Skip to content

Commit 4822696

Browse files
authored
feat(datafusion): consolidate DML WriteBuilder usage (#384)
1 parent 8c0aeaf commit 4822696

6 files changed

Lines changed: 218 additions & 11 deletions

File tree

crates/integrations/datafusion/src/merge_into.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use datafusion::sql::sqlparser::ast::{
3737
use futures::TryStreamExt;
3838

3939
use paimon::spec::{datums_to_binary_row, extract_datum_from_arrow, CoreOptions};
40-
use paimon::table::{CopyOnWriteMergeWriter, DataSplitBuilder, Table};
40+
use paimon::table::{CopyOnWriteMergeWriter, DataSplitBuilder, Table, WriteBuilder};
4141

4242
use crate::error::to_datafusion_error;
4343
use crate::sql_context::SQLContext;
@@ -361,6 +361,7 @@ async fn execute_cow_merge_once(
361361
let mut temp_tracker = TempTableTracker::new(ctx);
362362
let (has_target_data, cow_table_name) =
363363
register_cow_target_table(ctx, table, &writer, &mut temp_tracker).await?;
364+
let wb = table.new_write_builder();
364365

365366
let merge_ctx = CowMergeContext {
366367
source_ref: &source_ref,
@@ -376,6 +377,7 @@ async fn execute_cow_merge_once(
376377
ctx,
377378
&clauses,
378379
&mut writer,
380+
&wb,
379381
table,
380382
&merge_ctx,
381383
&mut temp_tracker,
@@ -391,8 +393,7 @@ async fn execute_cow_merge_once(
391393
all_messages.extend(insert_messages);
392394

393395
if !all_messages.is_empty() {
394-
let commit = table.new_write_builder().new_commit();
395-
commit
396+
wb.new_commit()
396397
.commit(all_messages)
397398
.await
398399
.map_err(to_datafusion_error)?;
@@ -418,6 +419,7 @@ async fn execute_cow_merge_inner(
418419
ctx: &SQLContext,
419420
clauses: &CowMergeClauses,
420421
writer: &mut CopyOnWriteMergeWriter,
422+
wb: &WriteBuilder<'_>,
421423
table: &Table,
422424
merge_ctx: &CowMergeContext<'_>,
423425
temp_tracker: &mut TempTableTracker<'_>,
@@ -596,10 +598,7 @@ async fn execute_cow_merge_inner(
596598

597599
let insert_count: usize = insert_batches.iter().map(|b| b.num_rows()).sum();
598600
if insert_count > 0 {
599-
let mut table_write = table
600-
.new_write_builder()
601-
.new_write()
602-
.map_err(to_datafusion_error)?;
601+
let mut table_write = wb.new_write().map_err(to_datafusion_error)?;
603602
for batch in &insert_batches {
604603
table_write
605604
.write_arrow_batch(batch)

crates/integrations/datafusion/tests/common/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,21 @@ pub async fn dml_count(sql_context: &SQLContext, sql_str: &str) -> u64 {
138138
.value(0)
139139
}
140140

141+
#[allow(dead_code)]
142+
pub async fn assert_sql_error(sql_context: &SQLContext, sql: &str, expected_substring: &str) {
143+
let err_msg = match sql_context.sql(sql).await {
144+
Ok(df) => match df.collect().await {
145+
Ok(_) => panic!("Expected error containing '{expected_substring}', but got Ok"),
146+
Err(err) => err.to_string(),
147+
},
148+
Err(err) => err.to_string(),
149+
};
150+
assert!(
151+
err_msg.contains(expected_substring),
152+
"Error message '{err_msg}' does not contain '{expected_substring}'"
153+
);
154+
}
155+
141156
/// Collect (i32, i32, String) rows from batches, sorted by (col0, col1).
142157
#[allow(dead_code)]
143158
pub fn collect_int_int_str(batches: &[RecordBatch]) -> Vec<(i32, i32, String)> {

crates/integrations/datafusion/tests/delete_tests.rs

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ mod common;
2323

2424
use paimon_datafusion::SQLContext;
2525

26-
use common::{create_sql_context, create_test_env, dml_count, exec, query_int_str_int};
26+
use common::{
27+
assert_sql_error, create_sql_context, create_test_env, dml_count, exec, query_int_str_int,
28+
};
2729

2830
// ======================= Helpers =======================
2931

@@ -449,3 +451,72 @@ async fn test_delete_multiple_rows_from_single_commit() {
449451

450452
assert_eq!(query(&sql_context).await, vec![(1, "a".into(), 10)]);
451453
}
454+
455+
// ======================= Unsupported cases =======================
456+
457+
#[tokio::test]
458+
async fn test_delete_rejects_primary_key_table() {
459+
let (tmp, catalog) = create_test_env();
460+
let sql_context = create_sql_context(catalog).await;
461+
sql_context
462+
.sql("CREATE SCHEMA paimon.test_db")
463+
.await
464+
.unwrap();
465+
sql_context
466+
.sql(
467+
"CREATE TABLE paimon.test_db.pk_t (\
468+
id INT NOT NULL, name VARCHAR, PRIMARY KEY (id)\
469+
) WITH ('bucket' = '1')",
470+
)
471+
.await
472+
.unwrap();
473+
474+
assert_sql_error(
475+
&sql_context,
476+
"DELETE FROM paimon.test_db.pk_t WHERE id = 1",
477+
"DELETE on primary-key tables is not yet supported",
478+
)
479+
.await;
480+
drop(tmp);
481+
}
482+
483+
#[tokio::test]
484+
async fn test_delete_rejects_data_evolution_table() {
485+
let (tmp, catalog) = create_test_env();
486+
let sql_context = create_sql_context(catalog).await;
487+
sql_context
488+
.sql("CREATE SCHEMA paimon.test_db")
489+
.await
490+
.unwrap();
491+
sql_context
492+
.sql(
493+
"CREATE TABLE paimon.test_db.de_t (\
494+
id INT NOT NULL, name VARCHAR\
495+
) WITH (\
496+
'row-tracking.enabled' = 'true',\
497+
'data-evolution.enabled' = 'true'\
498+
)",
499+
)
500+
.await
501+
.unwrap();
502+
503+
assert_sql_error(
504+
&sql_context,
505+
"DELETE FROM paimon.test_db.de_t WHERE id = 1",
506+
"DELETE on data-evolution tables is not yet supported",
507+
)
508+
.await;
509+
drop(tmp);
510+
}
511+
512+
#[tokio::test]
513+
async fn test_delete_rejects_table_alias() {
514+
let (_tmp, sql_context) = setup().await;
515+
516+
assert_sql_error(
517+
&sql_context,
518+
"DELETE FROM paimon.test_db.t AS target WHERE id = 1",
519+
"Table alias 'target' in DELETE is not yet supported",
520+
)
521+
.await;
522+
}

crates/integrations/datafusion/tests/merge_into_tests.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,3 +1289,36 @@ async fn test_rejects_table_with_primary_keys() {
12891289
)
12901290
.await;
12911291
}
1292+
1293+
#[tokio::test]
1294+
async fn test_rejects_primary_key_table_without_data_evolution() {
1295+
let (_tmp, catalog) = create_test_env();
1296+
let sql_context = create_sql_context(catalog.clone()).await;
1297+
1298+
sql_context
1299+
.sql("CREATE SCHEMA paimon.test_db")
1300+
.await
1301+
.unwrap();
1302+
sql_context
1303+
.sql(
1304+
"CREATE TABLE paimon.test_db.pk_plain_target (\
1305+
id INT NOT NULL, name STRING, PRIMARY KEY (id)\
1306+
) WITH ('bucket' = '1')",
1307+
)
1308+
.await
1309+
.unwrap();
1310+
1311+
register_source(
1312+
&sql_context,
1313+
"CREATE TEMPORARY TABLE paimon.test_db.src_pk_plain AS SELECT * FROM (VALUES (1, 'ALICE')) AS t(id, name)",
1314+
)
1315+
.await;
1316+
1317+
assert_merge_error(
1318+
&sql_context,
1319+
"MERGE INTO paimon.test_db.pk_plain_target t USING paimon.test_db.src_pk_plain s ON t.id = s.id \
1320+
WHEN MATCHED THEN UPDATE SET name = s.name",
1321+
"primary-key tables without data-evolution",
1322+
)
1323+
.await;
1324+
}

crates/integrations/datafusion/tests/update_tests.rs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ mod common;
2323

2424
use paimon_datafusion::SQLContext;
2525

26-
use common::{create_sql_context, create_test_env, dml_count, exec, query_int_str_int};
26+
use common::{
27+
assert_sql_error, create_sql_context, create_test_env, dml_count, exec, query_int_str_int,
28+
};
2729

2830
// ======================= Helpers =======================
2931

@@ -475,3 +477,73 @@ async fn test_update_empty_table() {
475477
assert_eq!(cnt, 0);
476478
drop(tmp);
477479
}
480+
481+
// ======================= Unsupported cases =======================
482+
483+
#[tokio::test]
484+
async fn test_update_rejects_primary_key_table_without_data_evolution() {
485+
let (tmp, catalog) = create_test_env();
486+
let sql_context = create_sql_context(catalog).await;
487+
sql_context
488+
.sql("CREATE SCHEMA paimon.test_db")
489+
.await
490+
.unwrap();
491+
sql_context
492+
.sql(
493+
"CREATE TABLE paimon.test_db.pk_t (\
494+
id INT NOT NULL, name VARCHAR, PRIMARY KEY (id)\
495+
) WITH ('bucket' = '1')",
496+
)
497+
.await
498+
.unwrap();
499+
500+
assert_sql_error(
501+
&sql_context,
502+
"UPDATE paimon.test_db.pk_t SET name = 'x' WHERE id = 1",
503+
"primary-key tables without data-evolution",
504+
)
505+
.await;
506+
drop(tmp);
507+
}
508+
509+
#[tokio::test]
510+
async fn test_update_rejects_primary_key_table_with_data_evolution() {
511+
let (tmp, catalog) = create_test_env();
512+
let sql_context = create_sql_context(catalog).await;
513+
sql_context
514+
.sql("CREATE SCHEMA paimon.test_db")
515+
.await
516+
.unwrap();
517+
sql_context
518+
.sql(
519+
"CREATE TABLE paimon.test_db.pk_de_t (\
520+
id INT NOT NULL, name VARCHAR, PRIMARY KEY (id)\
521+
) WITH (\
522+
'bucket' = '1',\
523+
'row-tracking.enabled' = 'true',\
524+
'data-evolution.enabled' = 'true'\
525+
)",
526+
)
527+
.await
528+
.unwrap();
529+
530+
assert_sql_error(
531+
&sql_context,
532+
"UPDATE paimon.test_db.pk_de_t SET name = 'x' WHERE id = 1",
533+
"does not support primary keys",
534+
)
535+
.await;
536+
drop(tmp);
537+
}
538+
539+
#[tokio::test]
540+
async fn test_update_rejects_table_alias() {
541+
let (_tmp, sql_context) = setup().await;
542+
543+
assert_sql_error(
544+
&sql_context,
545+
"UPDATE paimon.test_db.t AS target SET name = 'x' WHERE id = 1",
546+
"Table alias 'target' in UPDATE is not yet supported",
547+
)
548+
.await;
549+
}

docs/src/sql.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,21 @@ ALTER TABLE IF EXISTS paimon.my_db.users ADD COLUMN age INT;
227227

228228
## DML
229229

230+
The table type determines which row-level DML operations are supported:
231+
232+
| Operation | Append-only table | Primary-key table | Data-evolution row-tracking table (no primary key) |
233+
|---|---|---|---|
234+
| `INSERT INTO` | Supported | Supported | Supported |
235+
| `INSERT OVERWRITE` | Supported | Supported | Supported |
236+
| `INSERT OVERWRITE ... PARTITION` | Supported for partitioned tables | Supported for partitioned tables | Supported for partitioned tables |
237+
| `TRUNCATE TABLE` | Supported | Supported | Supported |
238+
| `ALTER TABLE ... DROP PARTITION` | Supported for partitioned tables | Supported for partitioned tables | Supported for partitioned tables |
239+
| `UPDATE` | Supported via Copy-on-Write | Not supported | Supported via row-id update |
240+
| `DELETE` | Supported via Copy-on-Write | Not supported | Not supported |
241+
| `MERGE INTO` | Supported via Copy-on-Write | Not supported | Supported for matched `UPDATE` and not-matched `INSERT`; matched `DELETE` is not supported |
242+
243+
A data-evolution row-tracking table must have both `'data-evolution.enabled' = 'true'` and `'row-tracking.enabled' = 'true'`, and must not have primary keys. Primary-key row-level `UPDATE`, `DELETE`, and `MERGE INTO` are not supported even when data evolution is enabled.
244+
230245
### INSERT INTO
231246

232247
```sql
@@ -279,7 +294,7 @@ For append-only tables (no primary key), updates are executed using Copy-on-Writ
279294
UPDATE paimon.my_db.t SET name = 'a_new' WHERE id = 1;
280295
```
281296

282-
For primary-key tables, `data-evolution.enabled` must be enabled to perform UPDATE.
297+
For data-evolution row-tracking tables without primary keys, updates are executed with row-id-based partial-column writes. Primary-key tables are not supported for `UPDATE`.
283298

284299
### DELETE
285300

@@ -289,6 +304,8 @@ For append-only tables, deletes are executed using Copy-on-Write:
289304
DELETE FROM paimon.my_db.t WHERE name = 'b';
290305
```
291306

307+
`DELETE` is not supported on primary-key tables or data-evolution tables.
308+
292309
### MERGE INTO
293310

294311
Standard SQL MERGE INTO syntax is supported, allowing INSERT, UPDATE, and DELETE in a single statement:
@@ -326,7 +343,7 @@ ON target.id = source.id
326343
WHEN MATCHED THEN UPDATE SET name = source.name;
327344
```
328345

329-
For data-evolution tables, MERGE INTO uses the `_ROW_ID` virtual column for row-level tracking. For append-only tables, it uses Copy-on-Write file rewriting.
346+
For append-only tables, `MERGE INTO` uses Copy-on-Write file rewriting and supports matched `UPDATE`, matched `DELETE`, and not-matched `INSERT`. For data-evolution row-tracking tables without primary keys, `MERGE INTO` uses the `_ROW_ID` virtual column for row-level tracking and supports matched `UPDATE` plus not-matched `INSERT`; matched `DELETE` is not yet supported. Primary-key tables are not supported for `MERGE INTO`.
330347

331348
### TRUNCATE TABLE
332349

0 commit comments

Comments
 (0)