diff --git a/tests-fuzz/Cargo.toml b/tests-fuzz/Cargo.toml index c7e733448988..af6f188a4f3f 100644 --- a/tests-fuzz/Cargo.toml +++ b/tests-fuzz/Cargo.toml @@ -71,6 +71,13 @@ test = false bench = false doc = false +[[bin]] +name = "fuzz_delete" +path = "targets/fuzz_delete.rs" +test = false +bench = false +doc = false + [[bin]] name = "fuzz_insert_logical_table" path = "targets/fuzz_insert_logical_table.rs" diff --git a/tests-fuzz/README.md b/tests-fuzz/README.md index 780107a65002..7b31a09c9475 100644 --- a/tests-fuzz/README.md +++ b/tests-fuzz/README.md @@ -36,6 +36,11 @@ cargo fuzz list --fuzz-dir tests-fuzz cargo fuzz run fuzz_create_table --fuzz-dir tests-fuzz ``` +> Note: if you meet the error, you may need to run : +> ```bash +> cargo fuzz run fuzz_create_table --fuzz-dir tests-fuzz -D -s none +> ``` + ## Crash Reproduction If you want to reproduce a crash, you first need to obtain the Base64 encoded code, which usually appears at the end of a crash report, and store it in a file. diff --git a/tests-fuzz/src/generator.rs b/tests-fuzz/src/generator.rs index 2f9de0770c98..d1b52ed26d26 100644 --- a/tests-fuzz/src/generator.rs +++ b/tests-fuzz/src/generator.rs @@ -16,6 +16,7 @@ pub mod alter_expr; pub mod create_expr; pub mod insert_expr; pub mod select_expr; +pub mod delete_expr; use std::fmt; diff --git a/tests-fuzz/src/generator/delete_expr.rs b/tests-fuzz/src/generator/delete_expr.rs new file mode 100644 index 000000000000..23ff0b618bbc --- /dev/null +++ b/tests-fuzz/src/generator/delete_expr.rs @@ -0,0 +1,64 @@ +use std::marker::PhantomData; + +use derive_builder::Builder; +use rand::seq::SliceRandom; +use rand::Rng; + +use crate::context::TableContextRef; +use crate::error::{Error, Result}; +use crate::generator::Generator; +use crate::ir::delete_expr::{DeleteExpr, WhereExpr}; +use crate::ir::generate_random_value; +use crate::ir::insert_expr::RowValue; + +#[derive(Builder)] +#[builder(pattern = "owned")] +pub struct DeleteExprGenerator { + table_ctx: TableContextRef, + #[builder(default)] + _phantom: PhantomData, +} + +impl Generator for DeleteExprGenerator { + type Error = Error; + + fn generate(&self, rng: &mut R) -> Result { + let filter_columns = self + .table_ctx + .columns + .iter() + .filter(|col| !col.is_nullable() && !col.has_default_value()) + .cloned() + .collect::>(); + if filter_columns.is_empty() { + return Ok(DeleteExpr::default()); + } + let selection = if filter_columns.len() > 1 { + rng.gen_range(1..filter_columns.len()) + } else { + 1 + }; + let mut selected_columns = filter_columns + .choose_multiple(rng, selection) + .cloned() + .collect::>(); + selected_columns.shuffle(rng); + + let mut where_clause = Vec::with_capacity(selected_columns.len()); + + for column in selected_columns.iter() { + let value = generate_random_value(rng, &column.column_type, None); + let condition = WhereExpr { + column: column.name.to_string(), + value: RowValue::Value(value), + }; + where_clause.push(condition); + } + + Ok(DeleteExpr { + table_name: self.table_ctx.name.to_string(), + columns: selected_columns, + where_clause, + }) + } +} diff --git a/tests-fuzz/src/ir.rs b/tests-fuzz/src/ir.rs index eb6ee105b2c8..168f01539b6a 100644 --- a/tests-fuzz/src/ir.rs +++ b/tests-fuzz/src/ir.rs @@ -18,6 +18,7 @@ pub(crate) mod alter_expr; pub(crate) mod create_expr; pub(crate) mod insert_expr; pub(crate) mod select_expr; +pub(crate) mod delete_expr; use core::fmt; @@ -29,6 +30,7 @@ use datatypes::types::TimestampType; use datatypes::value::Value; use derive_builder::Builder; pub use insert_expr::InsertIntoExpr; +pub use delete_expr::DeleteExpr; use lazy_static::lazy_static; use rand::seq::SliceRandom; use rand::Rng; diff --git a/tests-fuzz/src/ir/delete_expr.rs b/tests-fuzz/src/ir/delete_expr.rs new file mode 100644 index 000000000000..7e5f0af76898 --- /dev/null +++ b/tests-fuzz/src/ir/delete_expr.rs @@ -0,0 +1,28 @@ +// Copyright 2023 Greptime Team +// +// Licensed 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 super::insert_expr::RowValue; +use crate::ir::Column; + +pub struct WhereExpr { + pub column: String, + pub value: RowValue, +} + +#[derive(Default)] +pub struct DeleteExpr { + pub table_name: String, + pub columns: Vec, + pub where_clause: Vec, +} diff --git a/tests-fuzz/src/translator/mysql.rs b/tests-fuzz/src/translator/mysql.rs index 0c2498a3218b..1a8ee9a50a51 100644 --- a/tests-fuzz/src/translator/mysql.rs +++ b/tests-fuzz/src/translator/mysql.rs @@ -16,3 +16,4 @@ pub mod alter_expr; pub mod create_expr; pub mod insert_expr; pub mod select_expr; +pub mod delete_expr; diff --git a/tests-fuzz/src/translator/mysql/delete_expr.rs b/tests-fuzz/src/translator/mysql/delete_expr.rs new file mode 100644 index 000000000000..afc3d9fc612d --- /dev/null +++ b/tests-fuzz/src/translator/mysql/delete_expr.rs @@ -0,0 +1,73 @@ +// Copyright 2023 Greptime Team +// +// Licensed 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 crate::error::{Error, Result}; +use crate::ir::delete_expr::DeleteExpr; +use crate::translator::DslTranslator; + +pub struct DeleteExprTranslator; + +impl DslTranslator for DeleteExprTranslator { + type Error = Error; + + fn translate(&self, input: &DeleteExpr) -> Result { + // Generating WHERE clause if exists + let where_clause = if !input.where_clause.is_empty() { + input + .where_clause + .iter() + .map(|where_expr| format!("{} = {}", where_expr.column, where_expr.value)) + .collect::>() + .join(" AND ") + } else { + "1".to_string() + }; + + Ok(format!( + "DELETE FROM {} WHERE {};", + input.table_name, where_clause, + )) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use rand::SeedableRng; + + use super::DeleteExprTranslator; + use crate::generator::delete_expr::DeleteExprGeneratorBuilder; + use crate::generator::Generator; + use crate::test_utils; + use crate::translator::DslTranslator; + + #[test] + fn test_delete_expr_translator() { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0); + + let test_ctx = test_utils::new_test_ctx(); + let delete_expr_generator = DeleteExprGeneratorBuilder::default() + .table_ctx(Arc::new(test_ctx)) + .build() + .unwrap(); + + let delete_expr = delete_expr_generator.generate(&mut rng).unwrap(); + let output = DeleteExprTranslator.translate(&delete_expr).unwrap(); + // println!("output: {}", output); + + let expected_output = "DELETE FROM test WHERE ts = '+104408-01-06 12:42:54.931+0000'"; + assert_eq!(output, expected_output); + } +} diff --git a/tests-fuzz/targets/fuzz_delete.rs b/tests-fuzz/targets/fuzz_delete.rs new file mode 100644 index 000000000000..cdd666277b1d --- /dev/null +++ b/tests-fuzz/targets/fuzz_delete.rs @@ -0,0 +1,192 @@ +// Copyright 2023 Greptime Team +// +// Licensed 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. + +#![no_main] +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(unused_variables)] + +use std::sync::Arc; + +use common_telemetry::info; +use libfuzzer_sys::arbitrary::{Arbitrary, Unstructured}; +use libfuzzer_sys::fuzz_target; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaChaRng; +use snafu::{ensure, ResultExt}; +use sqlx::{Executor, MySql, Pool}; +use tests_fuzz::context::{TableContext, TableContextRef}; +use tests_fuzz::error::{self, Result}; +use tests_fuzz::fake::{ + merge_two_word_map_fn, random_capitalize_map, uppercase_and_keyword_backtick_map, + MappedGenerator, WordGenerator, +}; +use tests_fuzz::generator::create_expr::CreateTableExprGeneratorBuilder; +use tests_fuzz::generator::delete_expr::DeleteExprGeneratorBuilder; +use tests_fuzz::generator::insert_expr::InsertExprGeneratorBuilder; +use tests_fuzz::generator::Generator; +use tests_fuzz::ir::{CreateTableExpr, DeleteExpr, InsertIntoExpr}; +use tests_fuzz::translator::mysql::create_expr::CreateTableExprTranslator; +use tests_fuzz::translator::mysql::delete_expr::DeleteExprTranslator; +use tests_fuzz::translator::mysql::insert_expr::InsertIntoExprTranslator; +use tests_fuzz::translator::DslTranslator; +use tests_fuzz::utils::{init_greptime_connections_via_env, Connections}; + +struct FuzzContext { + greptime: Pool, +} + +impl FuzzContext { + async fn close(self) { + self.greptime.close().await; + } +} + +#[derive(Copy, Clone, Debug)] +struct FuzzInput { + seed: u64, + columns: usize, + rows: usize, +} + +impl Arbitrary<'_> for FuzzInput { + fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result { + let seed = u.int_in_range(u64::MIN..=u64::MAX)?; + let mut rng = ChaChaRng::seed_from_u64(seed); + let columns = rng.gen_range(2..30); + let rows = rng.gen_range(1..5); + Ok(FuzzInput { + columns, + rows, + seed, + }) + } +} + +fn generate_create_expr( + input: FuzzInput, + rng: &mut R, +) -> Result { + let create_table_generator = CreateTableExprGeneratorBuilder::default() + .name_generator(Box::new(MappedGenerator::new( + WordGenerator, + merge_two_word_map_fn(random_capitalize_map, uppercase_and_keyword_backtick_map), + ))) + .columns(input.columns) + .engine("mito") + .build() + .unwrap(); + create_table_generator.generate(rng) +} + +fn generate_insert_expr( + input: FuzzInput, + rng: &mut R, + table_ctx: TableContextRef, +) -> Result { + let omit_column_list = rng.gen_bool(0.2); + + let insert_generator = InsertExprGeneratorBuilder::default() + .table_ctx(table_ctx) + .omit_column_list(omit_column_list) + .rows(input.rows) + .build() + .unwrap(); + insert_generator.generate(rng) +} + +fn generate_delete_expr( + input: FuzzInput, + rng: &mut R, + table_ctx: TableContextRef, +) -> Result { + let delete_generator = DeleteExprGeneratorBuilder::default() + .table_ctx(table_ctx) + .build() + .unwrap(); + delete_generator.generate(rng) +} + +async fn execute_delete(ctx: FuzzContext, input: FuzzInput) -> Result<()> { + info!("input: {input:?}"); + let mut rng = ChaChaRng::seed_from_u64(input.seed); + + let create_expr = generate_create_expr(input, &mut rng)?; + let translator = CreateTableExprTranslator; + let sql = translator.translate(&create_expr)?; + let _result = sqlx::query(&sql) + .execute(&ctx.greptime) + .await + .context(error::ExecuteQuerySnafu { sql: &sql })?; + + // Generate insert expr + let table_ctx = Arc::new(TableContext::from(&create_expr)); + let insert_expr = generate_insert_expr(input, &mut rng, table_ctx)?; + let translator = InsertIntoExprTranslator; + let sql = translator.translate(&insert_expr)?; + let result = ctx + .greptime + .execute(sql.as_str()) + .await + .context(error::ExecuteQuerySnafu { sql: &sql })?; + + ensure!( + result.rows_affected() == input.rows as u64, + error::AssertSnafu { + reason: format!( + "expected rows affected: {}, actual: {}", + input.rows, + result.rows_affected(), + ) + } + ); + + // Generate delete expr + let table_ctx = Arc::new(TableContext::from(&create_expr)); + let delete_expr = generate_delete_expr(input, &mut rng, table_ctx)?; + let translator = DeleteExprTranslator; + let sql = translator.translate(&delete_expr)?; + let result = ctx + .greptime + .execute(sql.as_str()) + .await + .context(error::ExecuteQuerySnafu { sql: &sql })?; + + // Cleans up + let sql = format!("DROP TABLE {}", create_expr.table_name); + let result = sqlx::query(&sql) + .execute(&ctx.greptime) + .await + .context(error::ExecuteQuerySnafu { sql })?; + info!( + "Drop table: {}\n\nResult: {result:?}\n\n", + create_expr.table_name + ); + ctx.close().await; + + Ok(()) +} + +fuzz_target!(|input: FuzzInput| { + common_telemetry::init_default_ut_logging(); + common_runtime::block_on_write(async { + let Connections { mysql } = init_greptime_connections_via_env().await; + let ctx = FuzzContext { + greptime: mysql.expect("mysql connection init must be succeed"), + }; + execute_delete(ctx, input) + .await + .unwrap_or_else(|err| panic!("fuzz test must be succeed: {err:?}")); + }) +});