From e4333969b48292e4b00f00f75a2657b92be0c98a Mon Sep 17 00:00:00 2001 From: Weny Xu Date: Wed, 13 Mar 2024 22:11:47 +0800 Subject: [PATCH] feat(fuzz): add alter table target (#3503) * feat(fuzz): validate semantic type of column * feat(fuzz): add fuzz_alter_table target * feat(fuzz): validate columns * chore(ci): add fuzz_alter_table ci cfg --- .github/workflows/develop.yml | 2 +- tests-fuzz/Cargo.toml | 7 + tests-fuzz/src/context.rs | 189 +++++++++++++++++++++++- tests-fuzz/src/generator/alter_expr.rs | 47 +++++- tests-fuzz/src/ir/create_expr.rs | 2 +- tests-fuzz/src/validator/column.rs | 59 +++++++- tests-fuzz/targets/fuzz_alter_table.rs | 185 +++++++++++++++++++++++ tests-fuzz/targets/fuzz_create_table.rs | 6 +- 8 files changed, 480 insertions(+), 17 deletions(-) create mode 100644 tests-fuzz/targets/fuzz_alter_table.rs diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 94ada0fabf73..1ce1d8c18cd2 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -123,7 +123,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - target: [ "fuzz_create_table" ] + target: [ "fuzz_create_table", "fuzz_alter_table" ] steps: - uses: actions/checkout@v4 - uses: arduino/setup-protoc@v3 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..1a9d4b965b15 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":"animI","quote_style":null},"alter_options":{"AddColumn":{"column":{"name":{"value":"velit","quote_style":null},"column_type":{"Int32":{}},"options":[{"DefaultValue":{"Int32":1606462472}}]},"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":"animI","quote_style":null},"alter_options":{"RenameTable":{"new_table_name":{"value":"nihil","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":"animI","quote_style":null},"alter_options":{"DropColumn":{"name":{"value":"cUmquE","quote_style":null}}}}"#; assert_eq!(expected, serialized); } } diff --git a/tests-fuzz/src/ir/create_expr.rs b/tests-fuzz/src/ir/create_expr.rs index 6ef151f82558..1e6c165b5c8a 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, diff --git a/tests-fuzz/src/validator/column.rs b/tests-fuzz/src/validator/column.rs index 797834eec517..0736bbb48da4 100644 --- a/tests-fuzz/src/validator/column.rs +++ b/tests-fuzz/src/validator/column.rs @@ -37,6 +37,21 @@ fn is_nullable(str: &str) -> bool { str.to_uppercase() == "YES" } +enum SemanticType { + Timestamp, + Field, + Tag, +} + +fn semantic_type(str: &str) -> Option { + match str { + "TIMESTAMP" => Some(SemanticType::Timestamp), + "FIELD" => Some(SemanticType::Field), + "TAG" => Some(SemanticType::Tag), + _ => None, + } +} + impl PartialEq for ColumnEntry { fn eq(&self, other: &Column) -> bool { // Checks `table_name` @@ -108,11 +123,47 @@ impl PartialEq for ColumnEntry { .iter() .any(|opt| matches!(opt, ColumnOption::NotNull | ColumnOption::TimeIndex)) { - debug!("unexpected ColumnOption::NotNull or ColumnOption::TimeIndex"); + debug!("ColumnOption::NotNull or ColumnOption::TimeIndex is not found"); 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 } @@ -186,7 +237,7 @@ mod tests { table_name: String::new(), column_name: "test".to_string(), data_type: ConcreteDataType::int8_datatype().name(), - semantic_type: String::new(), + semantic_type: "FIELD".to_string(), column_default: None, is_nullable: "Yes".to_string(), }; @@ -210,7 +261,7 @@ mod tests { table_name: String::new(), column_name: "test".to_string(), data_type: ConcreteDataType::int8_datatype().to_string(), - semantic_type: String::new(), + semantic_type: "FIELD".to_string(), column_default: Some("1".to_string()), is_nullable: "Yes".to_string(), }; @@ -226,7 +277,7 @@ mod tests { table_name: String::new(), column_name: "test".to_string(), data_type: ConcreteDataType::int8_datatype().to_string(), - semantic_type: String::new(), + semantic_type: "FIELD".to_string(), column_default: Some("Hello()".to_string()), is_nullable: "Yes".to_string(), }; diff --git a/tests-fuzz/targets/fuzz_alter_table.rs b/tests-fuzz/targets/fuzz_alter_table.rs new file mode 100644 index 000000000000..3d345c2f16e7 --- /dev/null +++ b/tests-fuzz/targets/fuzz_alter_table.rs @@ -0,0 +1,185 @@ +// 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}; +use tests_fuzz::validator; + +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()); + + // Validates columns + let mut column_entries = validator::column::fetch_columns( + &ctx.greptime, + "public".into(), + table_ctx.name.clone(), + ) + .await?; + column_entries.sort_by(|a, b| a.column_name.cmp(&b.column_name)); + 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)?; + } + + // 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:?}")); + }) +}); diff --git a/tests-fuzz/targets/fuzz_create_table.rs b/tests-fuzz/targets/fuzz_create_table.rs index 7af489b1c2e3..6d351778dc96 100644 --- a/tests-fuzz/targets/fuzz_create_table.rs +++ b/tests-fuzz/targets/fuzz_create_table.rs @@ -33,7 +33,6 @@ use tests_fuzz::translator::mysql::create_expr::CreateTableExprTranslator; use tests_fuzz::translator::DslTranslator; use tests_fuzz::utils::{init_greptime_connections, Connections}; use tests_fuzz::validator; -use tests_fuzz::validator::column::fetch_columns; struct FuzzContext { greptime: Pool, @@ -85,9 +84,10 @@ async fn execute_create_table(ctx: FuzzContext, input: FuzzInput) -> Result<()> .context(error::ExecuteQuerySnafu { sql: &sql })?; info!("Create table: {sql}, result: {result:?}"); - // Validate columns + // Validates columns let mut column_entries = - fetch_columns(&ctx.greptime, "public".into(), expr.table_name.clone()).await?; + validator::column::fetch_columns(&ctx.greptime, "public".into(), expr.table_name.clone()) + .await?; column_entries.sort_by(|a, b| a.column_name.cmp(&b.column_name)); let mut columns = expr.columns.clone(); columns.sort_by(|a, b| a.name.value.cmp(&b.name.value));