From 2bb2c24172d8ccc415a553d43294b5d4e6143445 Mon Sep 17 00:00:00 2001 From: WenyXu Date: Wed, 13 Mar 2024 10:52:56 +0000 Subject: [PATCH] feat(fuzz): add fuzz_alter_table target --- tests-fuzz/Cargo.toml | 7 + tests-fuzz/src/context.rs | 189 ++++++++++++++++++++++++- tests-fuzz/src/generator/alter_expr.rs | 47 +++++- tests-fuzz/src/ir.rs | 40 ++++-- tests-fuzz/src/ir/create_expr.rs | 12 +- tests-fuzz/targets/fuzz_alter_table.rs | 173 ++++++++++++++++++++++ 6 files changed, 448 insertions(+), 20 deletions(-) create mode 100644 tests-fuzz/targets/fuzz_alter_table.rs diff --git a/tests-fuzz/Cargo.toml b/tests-fuzz/Cargo.toml index a63dc60babcd..ce216de41970 100644 --- a/tests-fuzz/Cargo.toml +++ b/tests-fuzz/Cargo.toml @@ -55,3 +55,10 @@ path = "targets/fuzz_insert.rs" test = false bench = false doc = false + +[[bin]] +name = "fuzz_alter_table" +path = "targets/fuzz_alter_table.rs" +test = false +bench = false +doc = false diff --git a/tests-fuzz/src/context.rs b/tests-fuzz/src/context.rs index a9fbcbd3aaa9..29536c853ccd 100644 --- a/tests-fuzz/src/context.rs +++ b/tests-fuzz/src/context.rs @@ -14,13 +14,20 @@ use std::sync::Arc; +use common_query::AddColumnLocation; use partition::partition::PartitionDef; +use rand::Rng; +use snafu::{ensure, OptionExt}; -use crate::ir::{Column, CreateTableExpr, Ident}; +use crate::error::{self, Result}; +use crate::generator::Random; +use crate::ir::alter_expr::AlterTableOperation; +use crate::ir::{AlterTableExpr, Column, CreateTableExpr, Ident}; pub type TableContextRef = Arc; /// TableContext stores table info. +#[derive(Debug, Clone)] pub struct TableContext { pub name: Ident, pub columns: Vec, @@ -48,3 +55,183 @@ impl From<&CreateTableExpr> for TableContext { } } } + +impl TableContext { + /// Applies the [AlterTableExpr]. + pub fn alter(mut self, expr: AlterTableExpr) -> Result { + match expr.alter_options { + AlterTableOperation::AddColumn { column, location } => { + ensure!( + !self.columns.iter().any(|col| col.name == column.name), + error::UnexpectedSnafu { + violated: format!("Column {} exists", column.name), + } + ); + match location { + Some(AddColumnLocation::First) => { + let mut columns = Vec::with_capacity(self.columns.len() + 1); + columns.push(column); + columns.extend(self.columns); + self.columns = columns; + } + Some(AddColumnLocation::After { column_name }) => { + let index = self + .columns + .iter() + // TODO(weny): find a better way? + .position(|col| col.name.to_string() == column_name) + .context(error::UnexpectedSnafu { + violated: format!("Column: {column_name} not found"), + })?; + self.columns.insert(index + 1, column); + } + None => self.columns.push(column), + } + // Re-generates the primary_keys + self.primary_keys = self + .columns + .iter() + .enumerate() + .flat_map(|(idx, col)| { + if col.is_primary_key() { + Some(idx) + } else { + None + } + }) + .collect(); + Ok(self) + } + AlterTableOperation::DropColumn { name } => { + self.columns.retain(|col| col.name != name); + // Re-generates the primary_keys + self.primary_keys = self + .columns + .iter() + .enumerate() + .flat_map(|(idx, col)| { + if col.is_primary_key() { + Some(idx) + } else { + None + } + }) + .collect(); + Ok(self) + } + AlterTableOperation::RenameTable { new_table_name } => { + ensure!( + new_table_name != self.name, + error::UnexpectedSnafu { + violated: "The new table name is equal the current name", + } + ); + self.name = new_table_name; + Ok(self) + } + } + } + + pub fn generate_unique_column_name( + &self, + rng: &mut R, + generator: &dyn Random, + ) -> Ident { + let mut name = generator.gen(rng); + while self.columns.iter().any(|col| col.name.value == name.value) { + name = generator.gen(rng); + } + name + } + + pub fn generate_unique_table_name( + &self, + rng: &mut R, + generator: &dyn Random, + ) -> Ident { + let mut name = generator.gen(rng); + while self.name.value == name.value { + name = generator.gen(rng); + } + name + } +} + +#[cfg(test)] +mod tests { + use common_query::AddColumnLocation; + use datatypes::data_type::ConcreteDataType; + + use super::TableContext; + use crate::ir::alter_expr::AlterTableOperation; + use crate::ir::create_expr::ColumnOption; + use crate::ir::{AlterTableExpr, Column, Ident}; + + #[test] + fn test_table_context_alter() { + let table_ctx = TableContext { + name: "foo".into(), + columns: vec![], + partition: None, + primary_keys: vec![], + }; + // Add a column + let expr = AlterTableExpr { + table_name: "foo".into(), + alter_options: AlterTableOperation::AddColumn { + column: Column { + name: "a".into(), + column_type: ConcreteDataType::timestamp_microsecond_datatype(), + options: vec![ColumnOption::PrimaryKey], + }, + location: None, + }, + }; + let table_ctx = table_ctx.alter(expr).unwrap(); + assert_eq!(table_ctx.columns[0].name, Ident::new("a")); + assert_eq!(table_ctx.primary_keys, vec![0]); + + // Add a column at first + let expr = AlterTableExpr { + table_name: "foo".into(), + alter_options: AlterTableOperation::AddColumn { + column: Column { + name: "b".into(), + column_type: ConcreteDataType::timestamp_microsecond_datatype(), + options: vec![ColumnOption::PrimaryKey], + }, + location: Some(AddColumnLocation::First), + }, + }; + let table_ctx = table_ctx.alter(expr).unwrap(); + assert_eq!(table_ctx.columns[0].name, Ident::new("b")); + assert_eq!(table_ctx.primary_keys, vec![0, 1]); + + // Add a column after "b" + let expr = AlterTableExpr { + table_name: "foo".into(), + alter_options: AlterTableOperation::AddColumn { + column: Column { + name: "c".into(), + column_type: ConcreteDataType::timestamp_microsecond_datatype(), + options: vec![ColumnOption::PrimaryKey], + }, + location: Some(AddColumnLocation::After { + column_name: "b".into(), + }), + }, + }; + let table_ctx = table_ctx.alter(expr).unwrap(); + assert_eq!(table_ctx.columns[1].name, Ident::new("c")); + assert_eq!(table_ctx.primary_keys, vec![0, 1, 2]); + + // Drop the column "b" + let expr = AlterTableExpr { + table_name: "foo".into(), + alter_options: AlterTableOperation::DropColumn { name: "b".into() }, + }; + let table_ctx = table_ctx.alter(expr).unwrap(); + assert_eq!(table_ctx.columns[1].name, Ident::new("a")); + assert_eq!(table_ctx.primary_keys, vec![0, 1]); + } +} diff --git a/tests-fuzz/src/generator/alter_expr.rs b/tests-fuzz/src/generator/alter_expr.rs index 03e823773d05..9c70cc47ad50 100644 --- a/tests-fuzz/src/generator/alter_expr.rs +++ b/tests-fuzz/src/generator/alter_expr.rs @@ -15,6 +15,7 @@ use std::marker::PhantomData; use common_query::AddColumnLocation; +use datatypes::data_type::ConcreteDataType; use derive_builder::Builder; use rand::Rng; use snafu::ensure; @@ -24,10 +25,38 @@ use crate::error::{self, Error, Result}; use crate::fake::WordGenerator; use crate::generator::{ColumnOptionGenerator, ConcreteDataTypeGenerator, Generator, Random}; use crate::ir::alter_expr::{AlterTableExpr, AlterTableOperation}; +use crate::ir::create_expr::ColumnOption; use crate::ir::{ - column_options_generator, droppable_columns, generate_columns, ColumnTypeGenerator, Ident, + droppable_columns, generate_columns, generate_random_value, ColumnTypeGenerator, Ident, }; +fn add_column_options_generator( + rng: &mut R, + column_type: &ConcreteDataType, +) -> Vec { + // 0 -> NULL + // 1 -> DEFAULT VALUE + // 2 -> PRIMARY KEY + DEFAULT VALUE + let idx = rng.gen_range(0..3); + match idx { + 0 => vec![ColumnOption::Null], + 1 => { + vec![ColumnOption::DefaultValue(generate_random_value( + rng, + column_type, + None, + ))] + } + 2 => { + vec![ + ColumnOption::PrimaryKey, + ColumnOption::DefaultValue(generate_random_value(rng, column_type, None)), + ] + } + _ => unreachable!(), + } +} + /// Generates the [AlterTableOperation::AddColumn] of [AlterTableExpr]. #[derive(Builder)] #[builder(pattern = "owned")] @@ -37,7 +66,7 @@ pub struct AlterExprAddColumnGenerator { location: bool, #[builder(default = "Box::new(WordGenerator)")] name_generator: Box>, - #[builder(default = "Box::new(column_options_generator)")] + #[builder(default = "Box::new(add_column_options_generator)")] column_options_generator: ColumnOptionGenerator, #[builder(default = "Box::new(ColumnTypeGenerator)")] column_type_generator: ConcreteDataTypeGenerator, @@ -65,7 +94,9 @@ impl Generator for AlterExprAddColumnGenera None }; - let name = self.name_generator.gen(rng); + let name = self + .table_ctx + .generate_unique_column_name(rng, self.name_generator.as_ref()); let column = generate_columns( rng, vec![name], @@ -116,7 +147,9 @@ impl Generator for AlterExprRenameGenerator { type Error = Error; fn generate(&self, rng: &mut R) -> Result { - let new_table_name = self.name_generator.gen(rng); + let new_table_name = self + .table_ctx + .generate_unique_table_name(rng, self.name_generator.as_ref()); Ok(AlterTableExpr { table_name: self.table_ctx.name.clone(), alter_options: AlterTableOperation::RenameTable { new_table_name }, @@ -153,7 +186,7 @@ mod tests { .generate(&mut rng) .unwrap(); let serialized = serde_json::to_string(&expr).unwrap(); - let expected = r#"{"table_name":{"value":"animI","quote_style":null},"alter_options":{"AddColumn":{"column":{"name":{"value":"velit","quote_style":null},"column_type":{"Int32":{}},"options":[{"DefaultValue":{"Int32":853246610}}]},"location":null}}}"#; + let expected = r#"{"table_name":{"value":"IuRe","quote_style":null},"alter_options":{"AddColumn":{"column":{"name":{"value":"voluptas","quote_style":null},"column_type":{"Int32":{}},"options":["PrimaryKey",{"DefaultValue":{"Int32":-343607799}}]},"location":null}}}"#; assert_eq!(expected, serialized); let expr = AlterExprRenameGeneratorBuilder::default() @@ -163,7 +196,7 @@ mod tests { .generate(&mut rng) .unwrap(); let serialized = serde_json::to_string(&expr).unwrap(); - let expected = r#"{"table_name":{"value":"animI","quote_style":null},"alter_options":{"RenameTable":{"new_table_name":{"value":"iure","quote_style":null}}}}"#; + let expected = r#"{"table_name":{"value":"IuRe","quote_style":null},"alter_options":{"RenameTable":{"new_table_name":{"value":"dolorum","quote_style":null}}}}"#; assert_eq!(expected, serialized); let expr = AlterExprDropColumnGeneratorBuilder::default() @@ -173,7 +206,7 @@ mod tests { .generate(&mut rng) .unwrap(); let serialized = serde_json::to_string(&expr).unwrap(); - let expected = r#"{"table_name":{"value":"animI","quote_style":null},"alter_options":{"DropColumn":{"name":{"value":"toTAm","quote_style":null}}}}"#; + let expected = r#"{"table_name":{"value":"IuRe","quote_style":null},"alter_options":{"DropColumn":{"name":{"value":"ADIPisCI","quote_style":null}}}}"#; assert_eq!(expected, serialized); } } diff --git a/tests-fuzz/src/ir.rs b/tests-fuzz/src/ir.rs index d20df0fa33a8..ab4b4ac9cb5f 100644 --- a/tests-fuzz/src/ir.rs +++ b/tests-fuzz/src/ir.rs @@ -34,6 +34,7 @@ use rand::seq::SliceRandom; use rand::Rng; use serde::{Deserialize, Serialize}; +use self::create_expr::ColumnOptionType; use crate::generator::Random; use crate::impl_random; use crate::ir::create_expr::ColumnOption; @@ -274,17 +275,34 @@ pub fn column_options_generator( // 2 -> DEFAULT VALUE // 3 -> PRIMARY KEY // 4 -> EMPTY - let option_idx = rng.gen_range(0..5); - match option_idx { - 0 => vec![ColumnOption::Null], - 1 => vec![ColumnOption::NotNull], - 2 => vec![ColumnOption::DefaultValue(generate_random_value( - rng, - column_type, - None, - ))], - 3 => vec![ColumnOption::PrimaryKey], - _ => vec![], + make_column_options_generator(vec![ + ColumnOptionType::Null, + ColumnOptionType::NotNull, + ColumnOptionType::DefaultValue, + ColumnOptionType::PrimaryKey, + ])(rng, column_type) +} + +/// Generates one of [ColumnOption] in `allowed_column_options` or returns empty options. +pub fn make_column_options_generator( + allowed_column_options: Vec, +) -> impl Fn(&mut R, &ConcreteDataType) -> Vec { + move |rng, column_type| { + let idx = rng.gen_range(0..allowed_column_options.len() + 1); + if let Some(t) = allowed_column_options.get(idx) { + match t { + ColumnOptionType::Null => vec![ColumnOption::Null], + ColumnOptionType::NotNull => vec![ColumnOption::NotNull], + ColumnOptionType::DefaultValue => vec![ColumnOption::DefaultValue( + generate_random_value(rng, column_type, None), + )], + ColumnOptionType::PrimaryKey => vec![ColumnOption::PrimaryKey], + ColumnOptionType::DefaultFn => todo!(), + ColumnOptionType::TimeIndex => unreachable!(), + } + } else { + vec![] + } } } diff --git a/tests-fuzz/src/ir/create_expr.rs b/tests-fuzz/src/ir/create_expr.rs index 6ef151f82558..392ee7d63e9c 100644 --- a/tests-fuzz/src/ir/create_expr.rs +++ b/tests-fuzz/src/ir/create_expr.rs @@ -22,7 +22,7 @@ use serde::{Deserialize, Serialize}; use crate::ir::{Column, Ident}; -// The column options +/// The column options #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub enum ColumnOption { Null, @@ -33,6 +33,16 @@ pub enum ColumnOption { PrimaryKey, } +/// The type of [ColumnOption]. +pub enum ColumnOptionType { + Null, + NotNull, + DefaultValue, + DefaultFn, + TimeIndex, + PrimaryKey, +} + impl Display for ColumnOption { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/tests-fuzz/targets/fuzz_alter_table.rs b/tests-fuzz/targets/fuzz_alter_table.rs new file mode 100644 index 000000000000..b84cfe90b87c --- /dev/null +++ b/tests-fuzz/targets/fuzz_alter_table.rs @@ -0,0 +1,173 @@ +// 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] + +use std::sync::Arc; + +use arbitrary::{Arbitrary, Unstructured}; +use common_telemetry::info; +use libfuzzer_sys::fuzz_target; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaChaRng; +use snafu::ResultExt; +use sqlx::{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::alter_expr::{ + AlterExprAddColumnGeneratorBuilder, AlterExprDropColumnGeneratorBuilder, + AlterExprRenameGeneratorBuilder, +}; +use tests_fuzz::generator::create_expr::CreateTableExprGeneratorBuilder; +use tests_fuzz::generator::Generator; +use tests_fuzz::ir::{droppable_columns, AlterTableExpr, CreateTableExpr}; +use tests_fuzz::translator::mysql::alter_expr::AlterTableExprTranslator; +use tests_fuzz::translator::mysql::create_expr::CreateTableExprTranslator; +use tests_fuzz::translator::DslTranslator; +use tests_fuzz::utils::{init_greptime_connections, Connections}; + +struct FuzzContext { + greptime: Pool, +} + +impl FuzzContext { + async fn close(self) { + self.greptime.close().await; + } +} + +#[derive(Clone, Debug)] +struct FuzzInput { + seed: u64, + actions: usize, +} + +fn generate_create_table_expr(rng: &mut R) -> Result { + let columns = rng.gen_range(2..30); + 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(columns) + .engine("mito") + .build() + .unwrap(); + create_table_generator.generate(rng) +} + +fn generate_alter_table_expr( + table_ctx: TableContextRef, + rng: &mut R, +) -> Result { + let rename = rng.gen_bool(0.2); + if rename { + let expr_generator = AlterExprRenameGeneratorBuilder::default() + .table_ctx(table_ctx) + .name_generator(Box::new(MappedGenerator::new( + WordGenerator, + merge_two_word_map_fn(random_capitalize_map, uppercase_and_keyword_backtick_map), + ))) + .build() + .unwrap(); + expr_generator.generate(rng) + } else { + let drop_column = rng.gen_bool(0.5) && !droppable_columns(&table_ctx.columns).is_empty(); + if drop_column { + let expr_generator = AlterExprDropColumnGeneratorBuilder::default() + .table_ctx(table_ctx) + .build() + .unwrap(); + expr_generator.generate(rng) + } else { + let location = rng.gen_bool(0.5); + let expr_generator = AlterExprAddColumnGeneratorBuilder::default() + .table_ctx(table_ctx) + .location(location) + .build() + .unwrap(); + expr_generator.generate(rng) + } + } +} + +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 actions = rng.gen_range(1..256); + + Ok(FuzzInput { seed, actions }) + } +} + +async fn execute_alter_table(ctx: FuzzContext, input: FuzzInput) -> Result<()> { + info!("input: {input:?}"); + let mut rng = ChaChaRng::seed_from_u64(input.seed); + + // Create table + let expr = generate_create_table_expr(&mut rng).unwrap(); + let translator = CreateTableExprTranslator; + let sql = translator.translate(&expr)?; + let result = sqlx::query(&sql) + .execute(&ctx.greptime) + .await + .context(error::ExecuteQuerySnafu { sql: &sql })?; + info!("Create table: {sql}, result: {result:?}"); + + // Alter table actions + let mut table_ctx = Arc::new(TableContext::from(&expr)); + for _ in 0..input.actions { + let expr = generate_alter_table_expr(table_ctx.clone(), &mut rng).unwrap(); + let translator = AlterTableExprTranslator; + let sql = translator.translate(&expr)?; + let result = sqlx::query(&sql) + .execute(&ctx.greptime) + .await + .context(error::ExecuteQuerySnafu { sql: &sql })?; + info!("Alter table: {sql}, result: {result:?}"); + // Applies changes + table_ctx = Arc::new(Arc::unwrap_or_clone(table_ctx).alter(expr).unwrap()); + // TODO(weny): Validate columns + } + + // Cleans up + let table_name = table_ctx.name.clone(); + let sql = format!("DROP TABLE {}", table_name); + let result = sqlx::query(&sql) + .execute(&ctx.greptime) + .await + .context(error::ExecuteQuerySnafu { sql })?; + info!("Drop table: {}, result: {result:?}", 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().await; + let ctx = FuzzContext { + greptime: mysql.expect("mysql connection init must be succeed"), + }; + execute_alter_table(ctx, input) + .await + .unwrap_or_else(|err| panic!("fuzz test must be succeed: {err:?}")); + }) +});