| // 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. |
| |
| //! UPDATE integration tests for append-only tables (CoW path). |
| //! |
| //! Test cases adapted from Java Paimon's `UpdateTableTestBase.scala`. |
| |
| mod common; |
| |
| use paimon_datafusion::SQLContext; |
| |
| use common::{ |
| assert_sql_error, create_sql_context, create_test_env, dml_count, exec, query_int_str_int, |
| }; |
| |
| // ======================= Helpers ======================= |
| |
| async fn setup() -> (tempfile::TempDir, SQLContext) { |
| let (tmp, catalog) = create_test_env(); |
| let sql_context = create_sql_context(catalog).await; |
| sql_context |
| .sql("CREATE SCHEMA paimon.test_db") |
| .await |
| .unwrap(); |
| sql_context |
| .sql("CREATE TABLE paimon.test_db.t (id INT, name VARCHAR, age INT)") |
| .await |
| .unwrap(); |
| exec( |
| &sql_context, |
| "INSERT INTO paimon.test_db.t VALUES (1, 'a', 10), (2, 'b', 20), (3, 'c', 30), (4, 'd', 40)", |
| ) |
| .await; |
| (tmp, sql_context) |
| } |
| |
| async fn setup_partitioned() -> (tempfile::TempDir, SQLContext) { |
| let (tmp, catalog) = create_test_env(); |
| let sql_context = create_sql_context(catalog).await; |
| sql_context |
| .sql("CREATE SCHEMA paimon.test_db") |
| .await |
| .unwrap(); |
| sql_context |
| .sql("CREATE TABLE paimon.test_db.t (id INT, name VARCHAR, pt INT) PARTITIONED BY (pt)") |
| .await |
| .unwrap(); |
| exec( |
| &sql_context, |
| "INSERT INTO paimon.test_db.t VALUES (1, 'a', 1), (2, 'b', 1), (3, 'c', 2), (4, 'd', 2)", |
| ) |
| .await; |
| (tmp, sql_context) |
| } |
| |
| async fn query(sql_context: &SQLContext) -> Vec<(i32, String, i32)> { |
| query_int_str_int( |
| sql_context, |
| "SELECT id, name, age FROM paimon.test_db.t ORDER BY id", |
| ) |
| .await |
| } |
| |
| async fn query_pt(sql_context: &SQLContext) -> Vec<(i32, String, i32)> { |
| query_int_str_int( |
| sql_context, |
| "SELECT id, name, pt FROM paimon.test_db.t ORDER BY id", |
| ) |
| .await |
| } |
| |
| // ======================= Basic UPDATE ======================= |
| |
| #[tokio::test] |
| async fn test_update_with_where() { |
| let (_tmp, sql_context) = setup().await; |
| |
| exec( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET name = 'a_new' WHERE id = 1", |
| ) |
| .await; |
| |
| assert_eq!( |
| query(&sql_context).await, |
| vec![ |
| (1, "a_new".into(), 10), |
| (2, "b".into(), 20), |
| (3, "c".into(), 30), |
| (4, "d".into(), 40), |
| ] |
| ); |
| } |
| |
| #[tokio::test] |
| async fn test_update_with_expression() { |
| let (_tmp, sql_context) = setup().await; |
| |
| exec( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET age = age + 1 WHERE id = 1", |
| ) |
| .await; |
| |
| assert_eq!( |
| query(&sql_context).await, |
| vec![ |
| (1, "a".into(), 11), |
| (2, "b".into(), 20), |
| (3, "c".into(), 30), |
| (4, "d".into(), 40), |
| ] |
| ); |
| } |
| |
| #[tokio::test] |
| async fn test_update_no_match_empty_commit() { |
| let (_tmp, sql_context) = setup().await; |
| |
| let cnt = dml_count( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET name = 'x' WHERE id = 99", |
| ) |
| .await; |
| assert_eq!(cnt, 0); |
| |
| assert_eq!( |
| query(&sql_context).await, |
| vec![ |
| (1, "a".into(), 10), |
| (2, "b".into(), 20), |
| (3, "c".into(), 30), |
| (4, "d".into(), 40), |
| ] |
| ); |
| } |
| |
| #[tokio::test] |
| async fn test_update_without_where() { |
| let (_tmp, sql_context) = setup().await; |
| |
| exec(&sql_context, "UPDATE paimon.test_db.t SET age = 0").await; |
| |
| assert_eq!( |
| query(&sql_context).await, |
| vec![ |
| (1, "a".into(), 0), |
| (2, "b".into(), 0), |
| (3, "c".into(), 0), |
| (4, "d".into(), 0), |
| ] |
| ); |
| } |
| |
| // ======================= Partitioned table UPDATE ======================= |
| |
| #[tokio::test] |
| async fn test_update_partitioned_by_non_partition_column() { |
| let (_tmp, sql_context) = setup_partitioned().await; |
| |
| exec( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET name = 'x' WHERE id = 1", |
| ) |
| .await; |
| |
| assert_eq!( |
| query_pt(&sql_context).await, |
| vec![ |
| (1, "x".into(), 1), |
| (2, "b".into(), 1), |
| (3, "c".into(), 2), |
| (4, "d".into(), 2), |
| ] |
| ); |
| } |
| |
| #[tokio::test] |
| async fn test_update_partitioned_by_partition_column() { |
| let (_tmp, sql_context) = setup_partitioned().await; |
| |
| exec( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET name = 'x' WHERE pt = 1", |
| ) |
| .await; |
| |
| assert_eq!( |
| query_pt(&sql_context).await, |
| vec![ |
| (1, "x".into(), 1), |
| (2, "x".into(), 1), |
| (3, "c".into(), 2), |
| (4, "d".into(), 2), |
| ] |
| ); |
| } |
| |
| #[tokio::test] |
| async fn test_update_partitioned_by_both_columns() { |
| let (_tmp, sql_context) = setup_partitioned().await; |
| |
| exec( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET name = 'x' WHERE pt = 2 AND id = 3", |
| ) |
| .await; |
| |
| assert_eq!( |
| query_pt(&sql_context).await, |
| vec![ |
| (1, "a".into(), 1), |
| (2, "b".into(), 1), |
| (3, "x".into(), 2), |
| (4, "d".into(), 2), |
| ] |
| ); |
| } |
| |
| // ======================= IN / NOT IN ======================= |
| |
| #[tokio::test] |
| async fn test_update_in_condition() { |
| let (_tmp, sql_context) = setup().await; |
| |
| exec( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET name = 'x' WHERE id IN (1, 3)", |
| ) |
| .await; |
| |
| assert_eq!( |
| query(&sql_context).await, |
| vec![ |
| (1, "x".into(), 10), |
| (2, "b".into(), 20), |
| (3, "x".into(), 30), |
| (4, "d".into(), 40), |
| ] |
| ); |
| } |
| |
| #[tokio::test] |
| async fn test_update_not_in_condition() { |
| let (_tmp, sql_context) = setup().await; |
| |
| exec( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET name = 'x' WHERE id NOT IN (1, 3)", |
| ) |
| .await; |
| |
| assert_eq!( |
| query(&sql_context).await, |
| vec![ |
| (1, "a".into(), 10), |
| (2, "x".into(), 20), |
| (3, "c".into(), 30), |
| (4, "x".into(), 40), |
| ] |
| ); |
| } |
| |
| // ======================= Cross-column references ======================= |
| |
| #[tokio::test] |
| async fn test_update_cross_column_reference() { |
| let (_tmp, sql_context) = setup().await; |
| |
| exec( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET age = id * 100 WHERE id <= 2", |
| ) |
| .await; |
| |
| assert_eq!( |
| query(&sql_context).await, |
| vec![ |
| (1, "a".into(), 100), |
| (2, "b".into(), 200), |
| (3, "c".into(), 30), |
| (4, "d".into(), 40), |
| ] |
| ); |
| } |
| |
| #[tokio::test] |
| async fn test_update_multiple_columns_with_expressions() { |
| let (_tmp, sql_context) = setup().await; |
| |
| exec( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET name = 'updated', age = age + id WHERE id >= 3", |
| ) |
| .await; |
| |
| assert_eq!( |
| query(&sql_context).await, |
| vec![ |
| (1, "a".into(), 10), |
| (2, "b".into(), 20), |
| (3, "updated".into(), 33), |
| (4, "updated".into(), 44), |
| ] |
| ); |
| } |
| |
| // ======================= Successive updates ======================= |
| |
| #[tokio::test] |
| async fn test_successive_updates() { |
| let (_tmp, sql_context) = setup().await; |
| |
| exec( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET age = 0 WHERE id = 1", |
| ) |
| .await; |
| exec( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET age = 99 WHERE id = 1", |
| ) |
| .await; |
| |
| assert_eq!( |
| query(&sql_context).await, |
| vec![ |
| (1, "a".into(), 99), |
| (2, "b".into(), 20), |
| (3, "c".into(), 30), |
| (4, "d".into(), 40), |
| ] |
| ); |
| } |
| |
| // ======================= OR condition ======================= |
| |
| #[tokio::test] |
| async fn test_update_or_condition() { |
| let (_tmp, sql_context) = setup().await; |
| |
| exec( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET name = 'x' WHERE id = 1 OR id = 4", |
| ) |
| .await; |
| |
| assert_eq!( |
| query(&sql_context).await, |
| vec![ |
| (1, "x".into(), 10), |
| (2, "b".into(), 20), |
| (3, "c".into(), 30), |
| (4, "x".into(), 40), |
| ] |
| ); |
| } |
| |
| // ======================= Range condition ======================= |
| |
| #[tokio::test] |
| async fn test_update_range_condition() { |
| let (_tmp, sql_context) = setup().await; |
| |
| exec( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET age = 0 WHERE id >= 2 AND id <= 3", |
| ) |
| .await; |
| |
| assert_eq!( |
| query(&sql_context).await, |
| vec![ |
| (1, "a".into(), 10), |
| (2, "b".into(), 0), |
| (3, "c".into(), 0), |
| (4, "d".into(), 40), |
| ] |
| ); |
| } |
| |
| // ======================= Subquery condition ======================= |
| |
| #[tokio::test] |
| async fn test_update_in_subquery() { |
| let (_tmp, sql_context) = setup().await; |
| |
| exec( |
| &sql_context, |
| "CREATE TEMPORARY TABLE paimon.test_db.src AS SELECT * FROM (VALUES (1), (3)) AS t(id)", |
| ) |
| .await; |
| |
| exec( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET name = 'sub' WHERE id IN (SELECT id FROM paimon.test_db.src)", |
| ) |
| .await; |
| |
| assert_eq!( |
| query(&sql_context).await, |
| vec![ |
| (1, "sub".into(), 10), |
| (2, "b".into(), 20), |
| (3, "sub".into(), 30), |
| (4, "d".into(), 40), |
| ] |
| ); |
| } |
| |
| // ======================= NULL handling ======================= |
| |
| #[tokio::test] |
| async fn test_update_with_null_values() { |
| let (tmp, catalog) = create_test_env(); |
| let sql_context = create_sql_context(catalog).await; |
| sql_context |
| .sql("CREATE SCHEMA paimon.test_db") |
| .await |
| .unwrap(); |
| sql_context |
| .sql("CREATE TABLE paimon.test_db.t (id INT, name VARCHAR, age INT)") |
| .await |
| .unwrap(); |
| sql_context |
| .sql("INSERT INTO paimon.test_db.t VALUES (1, 'a', 10), (2, NULL, 20), (3, 'c', NULL)") |
| .await |
| .unwrap() |
| .collect() |
| .await |
| .unwrap(); |
| |
| exec( |
| &sql_context, |
| "UPDATE paimon.test_db.t SET age = 0 WHERE name = 'a'", |
| ) |
| .await; |
| |
| let rows = query(&sql_context).await; |
| assert_eq!(rows[0], (1, "a".into(), 0)); |
| assert_eq!(rows[1].0, 2); |
| assert_eq!(rows[1].2, 20); |
| assert_eq!(rows[2].0, 3); |
| drop(tmp); |
| } |
| |
| // ======================= Empty table ======================= |
| |
| #[tokio::test] |
| async fn test_update_empty_table() { |
| let (tmp, catalog) = create_test_env(); |
| let sql_context = create_sql_context(catalog).await; |
| sql_context |
| .sql("CREATE SCHEMA paimon.test_db") |
| .await |
| .unwrap(); |
| sql_context |
| .sql("CREATE TABLE paimon.test_db.t (id INT, name VARCHAR, age INT)") |
| .await |
| .unwrap(); |
| |
| let cnt = dml_count(&sql_context, "UPDATE paimon.test_db.t SET name = 'x'").await; |
| assert_eq!(cnt, 0); |
| drop(tmp); |
| } |
| |
| // ======================= Unsupported cases ======================= |
| |
| #[tokio::test] |
| async fn test_update_rejects_primary_key_table_without_data_evolution() { |
| let (tmp, catalog) = create_test_env(); |
| let sql_context = create_sql_context(catalog).await; |
| sql_context |
| .sql("CREATE SCHEMA paimon.test_db") |
| .await |
| .unwrap(); |
| sql_context |
| .sql( |
| "CREATE TABLE paimon.test_db.pk_t (\ |
| id INT NOT NULL, name VARCHAR, PRIMARY KEY (id)\ |
| ) WITH ('bucket' = '1')", |
| ) |
| .await |
| .unwrap(); |
| |
| assert_sql_error( |
| &sql_context, |
| "UPDATE paimon.test_db.pk_t SET name = 'x' WHERE id = 1", |
| "primary-key tables without data-evolution", |
| ) |
| .await; |
| drop(tmp); |
| } |
| |
| #[tokio::test] |
| async fn test_update_rejects_primary_key_table_with_data_evolution() { |
| let (tmp, catalog) = create_test_env(); |
| let sql_context = create_sql_context(catalog).await; |
| sql_context |
| .sql("CREATE SCHEMA paimon.test_db") |
| .await |
| .unwrap(); |
| sql_context |
| .sql( |
| "CREATE TABLE paimon.test_db.pk_de_t (\ |
| id INT NOT NULL, name VARCHAR, PRIMARY KEY (id)\ |
| ) WITH (\ |
| 'bucket' = '1',\ |
| 'row-tracking.enabled' = 'true',\ |
| 'data-evolution.enabled' = 'true'\ |
| )", |
| ) |
| .await |
| .unwrap(); |
| |
| assert_sql_error( |
| &sql_context, |
| "UPDATE paimon.test_db.pk_de_t SET name = 'x' WHERE id = 1", |
| "does not support primary keys", |
| ) |
| .await; |
| drop(tmp); |
| } |
| |
| #[tokio::test] |
| async fn test_update_rejects_table_alias() { |
| let (_tmp, sql_context) = setup().await; |
| |
| assert_sql_error( |
| &sql_context, |
| "UPDATE paimon.test_db.t AS target SET name = 'x' WHERE id = 1", |
| "Table alias 'target' in UPDATE is not yet supported", |
| ) |
| .await; |
| } |