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
This commit is contained in:
Weny Xu
2024-03-13 22:11:47 +08:00
committed by GitHub
parent b55905cf66
commit e4333969b4
8 changed files with 480 additions and 17 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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>;
/// TableContext stores table info.
#[derive(Debug, Clone)]
pub struct TableContext {
pub name: Ident,
pub columns: Vec<Column>,
@@ -48,3 +55,183 @@ impl From<&CreateTableExpr> for TableContext {
}
}
}
impl TableContext {
/// Applies the [AlterTableExpr].
pub fn alter(mut self, expr: AlterTableExpr) -> Result<TableContext> {
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<R: Rng>(
&self,
rng: &mut R,
generator: &dyn Random<Ident, R>,
) -> 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<R: Rng>(
&self,
rng: &mut R,
generator: &dyn Random<Ident, R>,
) -> 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]);
}
}

View File

@@ -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<R: Rng>(
rng: &mut R,
column_type: &ConcreteDataType,
) -> Vec<ColumnOption> {
// 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<R: Rng + 'static> {
location: bool,
#[builder(default = "Box::new(WordGenerator)")]
name_generator: Box<dyn Random<Ident, R>>,
#[builder(default = "Box::new(column_options_generator)")]
#[builder(default = "Box::new(add_column_options_generator)")]
column_options_generator: ColumnOptionGenerator<R>,
#[builder(default = "Box::new(ColumnTypeGenerator)")]
column_type_generator: ConcreteDataTypeGenerator<R>,
@@ -65,7 +94,9 @@ impl<R: Rng + 'static> Generator<AlterTableExpr, R> 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<R: Rng> Generator<AlterTableExpr, R> for AlterExprRenameGenerator<R> {
type Error = Error;
fn generate(&self, rng: &mut R) -> Result<AlterTableExpr> {
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);
}
}

View File

@@ -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,

View File

@@ -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<SemanticType> {
match str {
"TIMESTAMP" => Some(SemanticType::Timestamp),
"FIELD" => Some(SemanticType::Field),
"TAG" => Some(SemanticType::Tag),
_ => None,
}
}
impl PartialEq<Column> for ColumnEntry {
fn eq(&self, other: &Column) -> bool {
// Checks `table_name`
@@ -108,11 +123,47 @@ impl PartialEq<Column> 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(),
};

View File

@@ -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<MySql>,
}
impl FuzzContext {
async fn close(self) {
self.greptime.close().await;
}
}
#[derive(Clone, Debug)]
struct FuzzInput {
seed: u64,
actions: usize,
}
fn generate_create_table_expr<R: Rng + 'static>(rng: &mut R) -> Result<CreateTableExpr> {
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<R: Rng + 'static>(
table_ctx: TableContextRef,
rng: &mut R,
) -> Result<AlterTableExpr> {
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<Self> {
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:?}"));
})
});

View File

@@ -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<MySql>,
@@ -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));