diff --git a/tests-fuzz/src/data/lorem_words b/tests-fuzz/src/data/lorem_words index 46246eaa47..ef5370fe86 100644 --- a/tests-fuzz/src/data/lorem_words +++ b/tests-fuzz/src/data/lorem_words @@ -97,7 +97,6 @@ eum iure reprehenderit qui -in ea voluptate velit @@ -138,10 +137,8 @@ unde omnis iste natus -error similique sunt -in culpa qui officia @@ -210,7 +207,6 @@ quo voluptas nulla pariatur -at vero eos et diff --git a/tests-fuzz/src/generator/alter_expr.rs b/tests-fuzz/src/generator/alter_expr.rs index 1a9d4b965b..5ab3af2dc8 100644 --- a/tests-fuzz/src/generator/alter_expr.rs +++ b/tests-fuzz/src/generator/alter_expr.rs @@ -27,7 +27,8 @@ use crate::generator::{ColumnOptionGenerator, ConcreteDataTypeGenerator, Generat use crate::ir::alter_expr::{AlterTableExpr, AlterTableOperation}; use crate::ir::create_expr::ColumnOption; use crate::ir::{ - droppable_columns, generate_columns, generate_random_value, ColumnTypeGenerator, Ident, + droppable_columns, generate_columns, generate_random_value, generate_random_value_abs, + ColumnTypeGenerator, Ident, }; fn add_column_options_generator( @@ -41,7 +42,7 @@ fn add_column_options_generator( match idx { 0 => vec![ColumnOption::Null], 1 => { - vec![ColumnOption::DefaultValue(generate_random_value( + vec![ColumnOption::DefaultValue(generate_random_value_abs( rng, column_type, None, @@ -50,7 +51,7 @@ fn add_column_options_generator( 2 => { vec![ ColumnOption::PrimaryKey, - ColumnOption::DefaultValue(generate_random_value(rng, column_type, None)), + ColumnOption::DefaultValue(generate_random_value_abs(rng, column_type, None)), ] } _ => unreachable!(), diff --git a/tests-fuzz/src/ir.rs b/tests-fuzz/src/ir.rs index 50ae1d216f..49af0d16b1 100644 --- a/tests-fuzz/src/ir.rs +++ b/tests-fuzz/src/ir.rs @@ -22,7 +22,8 @@ pub(crate) mod select_expr; use core::fmt; pub use alter_expr::AlterTableExpr; -use common_time::{Date, DateTime, Timestamp}; +use common_time::timestamp::TimeUnit; +use common_time::{Date, DateTime, Interval, Timestamp}; pub use create_expr::{CreateDatabaseExpr, CreateTableExpr}; use datatypes::data_type::ConcreteDataType; use datatypes::types::TimestampType; @@ -102,29 +103,65 @@ pub fn generate_random_value( } } +pub fn generate_random_value_abs( + rng: &mut R, + datatype: &ConcreteDataType, + random_str: Option<&dyn Random>, +) -> Value { + match datatype { + &ConcreteDataType::Boolean(_) => Value::from(rng.gen::()), + ConcreteDataType::Int16(_) => Value::from(i16::abs(rng.gen::())), + ConcreteDataType::Int32(_) => Value::from(i32::abs(rng.gen::())), + ConcreteDataType::Int64(_) => Value::from(i64::abs(rng.gen::())), + ConcreteDataType::Float32(_) => Value::from(f32::abs(rng.gen::())), + ConcreteDataType::Float64(_) => Value::from(f64::abs(rng.gen::())), + ConcreteDataType::String(_) => match random_str { + Some(random) => Value::from(random.gen(rng).value), + None => Value::from(rng.gen::().to_string()), + }, + ConcreteDataType::Date(_) => generate_random_date(rng), + ConcreteDataType::DateTime(_) => generate_random_datetime(rng), + &ConcreteDataType::Timestamp(ts_type) => generate_random_timestamp(rng, ts_type), + + _ => unimplemented!("unsupported type: {datatype}"), + } +} + fn generate_random_timestamp(rng: &mut R, ts_type: TimestampType) -> Value { let v = match ts_type { TimestampType::Second(_) => { - let min = i64::from(Timestamp::MIN_SECOND); - let max = i64::from(Timestamp::MAX_SECOND); + let now = Timestamp::current_time(TimeUnit::Second); + let min = now.sub_interval(Interval::from_year_month(12)).unwrap(); + let max = now.add_interval(Interval::from_year_month(12)).unwrap(); + let min = i64::from(min); + let max = i64::from(max); let value = rng.gen_range(min..=max); Timestamp::new_second(value) } TimestampType::Millisecond(_) => { - let min = i64::from(Timestamp::MIN_MILLISECOND); - let max = i64::from(Timestamp::MAX_MILLISECOND); + let now = Timestamp::current_time(TimeUnit::Millisecond); + let min = now.sub_interval(Interval::from_year_month(12)).unwrap(); + let max = now.add_interval(Interval::from_year_month(12)).unwrap(); + let min = i64::from(min); + let max = i64::from(max); let value = rng.gen_range(min..=max); Timestamp::new_millisecond(value) } TimestampType::Microsecond(_) => { - let min = i64::from(Timestamp::MIN_MICROSECOND); - let max = i64::from(Timestamp::MAX_MICROSECOND); + let now = Timestamp::current_time(TimeUnit::Microsecond); + let min = now.sub_interval(Interval::from_year_month(12)).unwrap(); + let max = now.add_interval(Interval::from_year_month(12)).unwrap(); + let min = i64::from(min); + let max = i64::from(max); let value = rng.gen_range(min..=max); Timestamp::new_microsecond(value) } TimestampType::Nanosecond(_) => { - let min = i64::from(Timestamp::MIN_NANOSECOND); - let max = i64::from(Timestamp::MAX_NANOSECOND); + let now = Timestamp::current_time(TimeUnit::Nanosecond); + let min = now.sub_interval(Interval::from_year_month(12)).unwrap(); + let max = now.add_interval(Interval::from_year_month(12)).unwrap(); + let min = i64::from(min); + let max = i64::from(max); let value = rng.gen_range(min..=max); Timestamp::new_nanosecond(value) } @@ -278,7 +315,7 @@ pub fn column_options_generator( match option_idx { 0 => vec![ColumnOption::Null], 1 => vec![ColumnOption::NotNull], - 2 => vec![ColumnOption::DefaultValue(generate_random_value( + 2 => vec![ColumnOption::DefaultValue(generate_random_value_abs( rng, column_type, None, diff --git a/tests-fuzz/src/validator/column.rs b/tests-fuzz/src/validator/column.rs index 349057817b..9e1b581f76 100644 --- a/tests-fuzz/src/validator/column.rs +++ b/tests-fuzz/src/validator/column.rs @@ -16,7 +16,9 @@ use common_telemetry::debug; use datatypes::data_type::DataType; use snafu::{ensure, ResultExt}; use sqlx::database::HasArguments; -use sqlx::{ColumnIndex, Database, Decode, Encode, Executor, IntoArguments, Type}; +use sqlx::{ + ColumnIndex, Database, Decode, Encode, Executor, IntoArguments, MySql, Pool, Row, Type, +}; use crate::error::{self, Result}; use crate::ir::create_expr::ColumnOption; @@ -24,12 +26,15 @@ use crate::ir::{Column, Ident}; #[derive(Debug, sqlx::FromRow)] pub struct ColumnEntry { - pub table_schema: String, - pub table_name: String, + #[sqlx(rename = "Field")] pub column_name: String, + #[sqlx(rename = "Type")] pub data_type: String, + #[sqlx(rename = "Semantic Type")] pub semantic_type: String, + #[sqlx(rename = "Default")] pub column_default: Option, + #[sqlx(rename = "Null")] pub is_nullable: String, } @@ -45,6 +50,7 @@ enum SemanticType { fn semantic_type(str: &str) -> Option { match str { + "TIME INDEX" => Some(SemanticType::Timestamp), "TIMESTAMP" => Some(SemanticType::Timestamp), "FIELD" => Some(SemanticType::Field), "TAG" => Some(SemanticType::Tag), @@ -127,43 +133,43 @@ impl PartialEq for ColumnEntry { return false; } } - //TODO: Checks `semantic_type` - match semantic_type(&self.semantic_type) { - Some(SemanticType::Tag) => { - if !other - .options - .iter() - .any(|opt| matches!(opt, ColumnOption::PrimaryKey)) - { - debug!("ColumnOption::PrimaryKey is not found"); - return false; - } - } - Some(SemanticType::Field) => { - if other - .options - .iter() - .any(|opt| matches!(opt, ColumnOption::PrimaryKey | ColumnOption::TimeIndex)) - { - debug!("unexpected ColumnOption::PrimaryKey or ColumnOption::TimeIndex"); - return false; - } - } - Some(SemanticType::Timestamp) => { - if !other - .options - .iter() - .any(|opt| matches!(opt, ColumnOption::TimeIndex)) - { - debug!("ColumnOption::TimeIndex is not found"); - return false; - } - } - None => { - debug!("unknown semantic type: {}", self.semantic_type); - return false; - } - }; + // //TODO: Checks `semantic_type` + // match semantic_type(&self.semantic_type) { + // Some(SemanticType::Tag) => { + // if !other + // .options + // .iter() + // .any(|opt| matches!(opt, ColumnOption::PrimaryKey)) + // { + // debug!("ColumnOption::PrimaryKey is not found"); + // return false; + // } + // } + // Some(SemanticType::Field) => { + // if other + // .options + // .iter() + // .any(|opt| matches!(opt, ColumnOption::PrimaryKey | ColumnOption::TimeIndex)) + // { + // debug!("unexpected ColumnOption::PrimaryKey or ColumnOption::TimeIndex"); + // return false; + // } + // } + // Some(SemanticType::Timestamp) => { + // if !other + // .options + // .iter() + // .any(|opt| matches!(opt, ColumnOption::TimeIndex)) + // { + // debug!("ColumnOption::TimeIndex is not found"); + // return false; + // } + // } + // None => { + // debug!("unknown semantic type: {}", self.semantic_type); + // return false; + // } + // }; true } @@ -211,13 +217,44 @@ where for<'c> String: Encode<'c, DB> + Type, for<'c> &'c str: ColumnIndex<::Row>, { - let sql = "SELECT table_schema, table_name, column_name, greptime_data_type as data_type, semantic_type, column_default, is_nullable FROM information_schema.columns WHERE table_schema = ? AND table_name = ?"; - sqlx::query_as::<_, ColumnEntry>(sql) - .bind(schema_name.value.to_string()) - .bind(table_name.value.to_string()) - .fetch_all(e) + // let sql = format!("DESC TABLE {table_name}"); + // let rows = sqlx::query(&sql) + // .fetch_all(e) + // .await + // .context(error::ExecuteQuerySnafu { sql })?; + + todo!() +} + +pub async fn fetch_columns_via_mysql( + db: &Pool, + _schema_name: Ident, + table_name: Ident, +) -> Result> { + let sql = format!("DESC TABLE {table_name}"); + let rows = sqlx::query(&sql) + .fetch_all(db) .await - .context(error::ExecuteQuerySnafu { sql }) + .context(error::ExecuteQuerySnafu { sql })?; + Ok(rows + .into_iter() + .map(|row| { + let default_value: String = row.get(3); + let column_default = if default_value.is_empty() { + None + } else { + Some(default_value) + }; + + ColumnEntry { + column_name: row.get(0), + data_type: row.get(1), + is_nullable: row.get(2), + column_default, + semantic_type: row.get(4), + } + }) + .collect::>()) } #[cfg(test)] @@ -233,8 +270,6 @@ mod tests { fn test_column_eq() { common_telemetry::init_default_ut_logging(); let column_entry = ColumnEntry { - table_schema: String::new(), - table_name: String::new(), column_name: "test".to_string(), data_type: ConcreteDataType::int8_datatype().name(), semantic_type: "FIELD".to_string(), @@ -257,8 +292,6 @@ mod tests { assert!(column_entry == column); // With default value let column_entry = ColumnEntry { - table_schema: String::new(), - table_name: String::new(), column_name: "test".to_string(), data_type: ConcreteDataType::int8_datatype().to_string(), semantic_type: "FIELD".to_string(), @@ -273,8 +306,6 @@ mod tests { assert!(column_entry == column); // With default function let column_entry = ColumnEntry { - table_schema: String::new(), - table_name: String::new(), column_name: "test".to_string(), data_type: ConcreteDataType::int8_datatype().to_string(), semantic_type: "FIELD".to_string(), diff --git a/tests-fuzz/targets/fuzz_alter_table.rs b/tests-fuzz/targets/fuzz_alter_table.rs index 3d345c2f16..1591d9ab28 100644 --- a/tests-fuzz/targets/fuzz_alter_table.rs +++ b/tests-fuzz/targets/fuzz_alter_table.rs @@ -21,8 +21,8 @@ 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 snafu::{ensure, ResultExt}; +use sqlx::{Executor, MySql, Pool}; use tests_fuzz::context::{TableContext, TableContextRef}; use tests_fuzz::error::{self, Result}; use tests_fuzz::fake::{ @@ -34,10 +34,12 @@ use tests_fuzz::generator::alter_expr::{ AlterExprRenameGeneratorBuilder, }; use tests_fuzz::generator::create_expr::CreateTableExprGeneratorBuilder; +use tests_fuzz::generator::insert_expr::InsertExprGeneratorBuilder; use tests_fuzz::generator::Generator; -use tests_fuzz::ir::{droppable_columns, AlterTableExpr, CreateTableExpr}; +use tests_fuzz::ir::{droppable_columns, AlterTableExpr, CreateTableExpr, InsertIntoExpr}; use tests_fuzz::translator::mysql::alter_expr::AlterTableExprTranslator; use tests_fuzz::translator::mysql::create_expr::CreateTableExprTranslator; +use tests_fuzz::translator::mysql::insert_expr::InsertIntoExprTranslator; use tests_fuzz::translator::DslTranslator; use tests_fuzz::utils::{init_greptime_connections, Connections}; use tests_fuzz::validator; @@ -52,19 +54,17 @@ impl FuzzContext { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Copy)] struct FuzzInput { seed: u64, actions: usize, + rows: 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), - ))) + .name_generator(Box::new(WordGenerator)) .columns(columns) .engine("mito") .build() @@ -80,10 +80,7 @@ fn generate_alter_table_expr( 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), - ))) + .name_generator(Box::new(WordGenerator)) .build() .unwrap(); expr_generator.generate(rng) @@ -112,11 +109,29 @@ impl Arbitrary<'_> for FuzzInput { 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); + let insertions = rng.gen_range(1..1024); - Ok(FuzzInput { seed, actions }) + Ok(FuzzInput { + seed, + actions, + rows: insertions, + }) } } +fn generate_insert_expr( + input: FuzzInput, + rng: &mut R, + table_ctx: TableContextRef, +) -> Result { + let insert_generator = InsertExprGeneratorBuilder::default() + .table_ctx(table_ctx) + .rows(input.rows) + .build() + .unwrap(); + insert_generator.generate(rng) +} + async fn execute_alter_table(ctx: FuzzContext, input: FuzzInput) -> Result<()> { info!("input: {input:?}"); let mut rng = ChaChaRng::seed_from_u64(input.seed); @@ -146,7 +161,7 @@ async fn execute_alter_table(ctx: FuzzContext, input: FuzzInput) -> Result<()> { table_ctx = Arc::new(Arc::unwrap_or_clone(table_ctx).alter(expr).unwrap()); // Validates columns - let mut column_entries = validator::column::fetch_columns( + let mut column_entries = validator::column::fetch_columns_via_mysql( &ctx.greptime, "public".into(), table_ctx.name.clone(), @@ -156,6 +171,28 @@ async fn execute_alter_table(ctx: FuzzContext, input: FuzzInput) -> Result<()> { let mut columns = table_ctx.columns.clone(); columns.sort_by(|a, b| a.name.value.cmp(&b.name.value)); validator::column::assert_eq(&column_entries, &columns)?; + + // insertions + let insert_expr = generate_insert_expr(input, &mut rng, table_ctx.clone())?; + let translator = InsertIntoExprTranslator; + let sql = translator.translate(&insert_expr)?; + let result = ctx + .greptime + // unprepared query, see + .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(), + ) + } + ); } // Cleans up