Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fuzz): validate columns #3485

Merged
merged 1 commit into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions tests-fuzz/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,7 @@ pub enum Error {
error: sqlx::error::Error,
location: Location,
},

#[snafu(display("Failed to assert: {}", reason))]
Assert { reason: String, location: Location },
}
9 changes: 9 additions & 0 deletions tests-fuzz/src/generator/create_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,19 @@ impl<R: Rng + 'static> Generator<CreateTableExpr, R> 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();
Expand Down
1 change: 1 addition & 0 deletions tests-fuzz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
15 changes: 15 additions & 0 deletions tests-fuzz/src/validator.rs
Original file line number Diff line number Diff line change
@@ -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;
240 changes: 240 additions & 0 deletions tests-fuzz/src/validator/column.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
// 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<String>,
pub is_nullable: String,
}

fn is_nullable(str: &str) -> bool {
str.to_uppercase() == "YES"
}

impl PartialEq<Column> 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<Vec<ColumnEntry>>
where
DB: Database,
<DB as HasArguments<'a>>::Arguments: IntoArguments<'a, DB>,
for<'c> E: 'a + Executor<'c, Database = DB>,
for<'c> String: Decode<'c, DB> + Type<DB>,
for<'c> String: Encode<'c, DB> + Type<DB>,
for<'c> &'c str: ColumnIndex<<DB as Database>::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, DataType};
use datatypes::value::Value;

use super::ColumnEntry;
use crate::ir::create_expr::ColumnOption;
use crate::ir::{Column, Ident};

#[test]
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: String::new(),
column_default: None,
is_nullable: "Yes".to_string(),
};
// 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: String::new(),
column_name: "test".to_string(),
data_type: ConcreteDataType::int8_datatype().to_string(),
semantic_type: String::new(),
column_default: Some("1".to_string()),
is_nullable: "Yes".to_string(),
};
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: String::new(),
column_name: "test".to_string(),
data_type: ConcreteDataType::int8_datatype().to_string(),
semantic_type: String::new(),
column_default: Some("Hello()".to_string()),
is_nullable: "Yes".to_string(),
};
let column = Column {
name: Ident::with_quote('\'', "test"),
column_type: ConcreteDataType::int8_datatype(),
options: vec![ColumnOption::DefaultFn("Hello()".to_string())],
};
assert!(column_entry == column);
}
}
15 changes: 13 additions & 2 deletions tests-fuzz/targets/fuzz_create_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MySql>,
Expand All @@ -52,7 +54,8 @@ struct FuzzInput {
impl Arbitrary<'_> for FuzzInput {
fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result<Self> {
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 })
}
}
Expand All @@ -64,7 +67,7 @@ fn generate_expr(input: FuzzInput) -> Result<CreateTableExpr> {
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();
Expand All @@ -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)
Expand Down