diff --git a/Cargo.lock b/Cargo.lock index c2e8b8b9f29e..3ec7ea27d9c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4815,9 +4815,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.151" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libfuzzer-sys" diff --git a/tests-fuzz/src/error.rs b/tests-fuzz/src/error.rs index 9cf7728b81d2..def6414cb13b 100644 --- a/tests-fuzz/src/error.rs +++ b/tests-fuzz/src/error.rs @@ -46,4 +46,7 @@ pub enum Error { error: sqlx::error::Error, location: Location, }, + + #[snafu(display("Failed to assert: {}", reason))] + Assert { reason: String, location: Location }, } diff --git a/tests-fuzz/src/generator/create_expr.rs b/tests-fuzz/src/generator/create_expr.rs index 534fac827bd2..3aeb03b9544c 100644 --- a/tests-fuzz/src/generator/create_expr.rs +++ b/tests-fuzz/src/generator/create_expr.rs @@ -189,10 +189,19 @@ impl Generator for CreateTableExprGenerato #[cfg(test)] mod tests { + use datatypes::value::Value; use rand::SeedableRng; use super::*; + #[test] + fn test_float64() { + let value = Value::from(0.047318541668048164); + assert_eq!("0.047318541668048164", value.to_string()); + let value: f64 = "0.047318541668048164".parse().unwrap(); + assert_eq!("0.047318541668048164", value.to_string()); + } + #[test] fn test_create_table_expr_generator() { let mut rng = rand::thread_rng(); diff --git a/tests-fuzz/src/lib.rs b/tests-fuzz/src/lib.rs index 2666a35051c1..406927d6b46b 100644 --- a/tests-fuzz/src/lib.rs +++ b/tests-fuzz/src/lib.rs @@ -22,6 +22,7 @@ pub mod generator; pub mod ir; pub mod translator; pub mod utils; +pub mod validator; #[cfg(test)] pub mod test_utils; diff --git a/tests-fuzz/src/validator.rs b/tests-fuzz/src/validator.rs new file mode 100644 index 000000000000..198d009a152b --- /dev/null +++ b/tests-fuzz/src/validator.rs @@ -0,0 +1,15 @@ +// 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. + +pub mod column; diff --git a/tests-fuzz/src/validator/column.rs b/tests-fuzz/src/validator/column.rs new file mode 100644 index 000000000000..f0c1ebe66f8a --- /dev/null +++ b/tests-fuzz/src/validator/column.rs @@ -0,0 +1,239 @@ +// 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 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 crate::error::{self, Result}; +use crate::ir::create_expr::ColumnOption; +use crate::ir::{Column, Ident}; + +#[derive(Debug, sqlx::FromRow)] +pub struct ColumnEntry { + pub table_schema: String, + pub table_name: String, + pub column_name: String, + pub data_type: String, + pub semantic_type: String, + pub column_default: Option, + pub is_nullable: String, +} + +fn is_nullable(str: &str) -> bool { + str.to_uppercase() == "YES" +} + +impl PartialEq for ColumnEntry { + fn eq(&self, other: &Column) -> bool { + // Checks `table_name` + if other.name.value != self.column_name { + debug!( + "expected name: {}, got: {}", + other.name.value, self.column_name + ); + return false; + } + // Checks `data_type` + if other.column_type.name() != self.data_type { + debug!( + "expected column_type: {}, got: {}", + other.column_type.name(), + self.data_type + ); + return false; + } + // Checks `column_default` + match &self.column_default { + Some(value) => { + let default_value_opt = other.options.iter().find(|opt| { + matches!( + opt, + ColumnOption::DefaultFn(_) | ColumnOption::DefaultValue(_) + ) + }); + if default_value_opt.is_none() { + debug!("default value options is not found"); + return false; + } + let default_value = match default_value_opt.unwrap() { + ColumnOption::DefaultValue(v) => v.to_string(), + ColumnOption::DefaultFn(f) => f.to_string(), + _ => unreachable!(), + }; + if &default_value != value { + debug!("expected default value: {default_value}, got: {value}"); + return false; + } + } + None => { + if other.options.iter().any(|opt| { + matches!( + opt, + ColumnOption::DefaultFn(_) | ColumnOption::DefaultValue(_) + ) + }) { + return false; + } + } + }; + // Checks `is_nullable` + if is_nullable(&self.is_nullable) { + // Null is the default value. Therefore, we only ensure there is no `ColumnOption::NotNull` option. + if other + .options + .iter() + .any(|opt| matches!(opt, ColumnOption::NotNull)) + { + debug!("ColumnOption::NotNull is not found"); + return false; + } + } else { + // `ColumnOption::TimeIndex` imply means the field is not nullable. + if !other + .options + .iter() + .any(|opt| matches!(opt, ColumnOption::NotNull | ColumnOption::TimeIndex)) + { + debug!("unexpected ColumnOption::NotNull or ColumnOption::TimeIndex"); + return false; + } + } + //TODO: Checks `semantic_type` + + true + } +} + +/// Asserts [&[ColumnEntry]] is equal to [&[Column]] +pub fn assert_eq(fetched_columns: &[ColumnEntry], columns: &[Column]) -> Result<()> { + ensure!( + columns.len() == fetched_columns.len(), + error::AssertSnafu { + reason: format!( + "Expected columns length: {}, got: {}", + columns.len(), + fetched_columns.len(), + ) + } + ); + + for (idx, fetched) in fetched_columns.iter().enumerate() { + ensure!( + fetched == &columns[idx], + error::AssertSnafu { + reason: format!( + "ColumnEntry {fetched:?} is not equal to Column {:?}", + columns[idx] + ) + } + ); + } + + Ok(()) +} + +/// Returns all [ColumnEntry] of the `table_name` from `information_schema`. +pub async fn fetch_columns<'a, DB, E>( + e: E, + schema_name: Ident, + table_name: Ident, +) -> Result> +where + DB: Database, + >::Arguments: IntoArguments<'a, DB>, + for<'c> E: 'a + Executor<'c, Database = DB>, + for<'c> String: Decode<'c, DB> + Type, + for<'c> String: Encode<'c, DB> + Type, + for<'c> &'c str: ColumnIndex<::Row>, +{ + let sql = "SELECT * 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) + .await + .context(error::ExecuteQuerySnafu { sql }) +} + +#[cfg(test)] +mod tests { + use datatypes::data_type::ConcreteDataType; + use datatypes::value::Value; + + use super::ColumnEntry; + use crate::ir::create_expr::ColumnOption; + use crate::ir::{Column, Ident}; + + #[test] + fn test_column_eq() { + let column_entry = ColumnEntry { + table_schema: String::new(), + table_name: "test".to_string(), + column_name: String::new(), + data_type: ConcreteDataType::int8_datatype().to_string(), + semantic_type: String::new(), + column_default: None, + is_nullable: String::new(), + }; + // Naive + let column = Column { + name: Ident::new("test"), + column_type: ConcreteDataType::int8_datatype(), + options: vec![], + }; + assert!(column_entry == column); + // With quote + let column = Column { + name: Ident::with_quote('\'', "test"), + column_type: ConcreteDataType::int8_datatype(), + options: vec![], + }; + assert!(column_entry == column); + // With default value + let column_entry = ColumnEntry { + table_schema: String::new(), + table_name: "test".to_string(), + column_name: String::new(), + data_type: ConcreteDataType::int8_datatype().to_string(), + semantic_type: String::new(), + column_default: Some("1".to_string()), + is_nullable: String::new(), + }; + let column = Column { + name: Ident::with_quote('\'', "test"), + column_type: ConcreteDataType::int8_datatype(), + options: vec![ColumnOption::DefaultValue(Value::from(1))], + }; + assert!(column_entry == column); + // With default function + let column_entry = ColumnEntry { + table_schema: String::new(), + table_name: "test".to_string(), + column_name: String::new(), + data_type: ConcreteDataType::int8_datatype().to_string(), + semantic_type: String::new(), + column_default: Some("Hello()".to_string()), + is_nullable: String::new(), + }; + let column = Column { + name: Ident::with_quote('\'', "test"), + column_type: ConcreteDataType::int8_datatype(), + options: vec![ColumnOption::DefaultFn("Hello()".to_string())], + }; + assert!(column_entry == column); + } +} diff --git a/tests-fuzz/targets/fuzz_create_table.rs b/tests-fuzz/targets/fuzz_create_table.rs index f3e3cdd7f252..7af489b1c2e3 100644 --- a/tests-fuzz/targets/fuzz_create_table.rs +++ b/tests-fuzz/targets/fuzz_create_table.rs @@ -32,6 +32,8 @@ use tests_fuzz::ir::CreateTableExpr; 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, @@ -52,7 +54,8 @@ struct FuzzInput { impl Arbitrary<'_> for FuzzInput { fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result { let seed = u.int_in_range(u64::MIN..=u64::MAX)?; - let columns = u.int_in_range(2..=10)?; + let mut rng = ChaChaRng::seed_from_u64(seed); + let columns = rng.gen_range(2..30); Ok(FuzzInput { columns, seed }) } } @@ -64,7 +67,7 @@ fn generate_expr(input: FuzzInput) -> Result { WordGenerator, merge_two_word_map_fn(random_capitalize_map, uppercase_and_keyword_backtick_map), ))) - .columns(rng.gen_range(1..input.columns)) + .columns(input.columns) .engine("mito") .build() .unwrap(); @@ -82,6 +85,14 @@ async fn execute_create_table(ctx: FuzzContext, input: FuzzInput) -> Result<()> .context(error::ExecuteQuerySnafu { sql: &sql })?; info!("Create table: {sql}, result: {result:?}"); + // Validate columns + let mut column_entries = + 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)); + validator::column::assert_eq(&column_entries, &columns)?; + // Cleans up let sql = format!("DROP TABLE {}", expr.table_name); let result = sqlx::query(&sql)