diff --git a/docs/fuzzing.md b/docs/fuzzing.md index 3d1655106..5028f3ebf 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -27,6 +27,10 @@ cd fuzz cargo +nightly fuzz run fuzz_parse_sql -- -max_total_time=600 ``` +There are two targets. `fuzz_parse_sql` parses the input with every dialect. +`fuzz_parse_roundtrip` additionally re-parses the SQL rendered by `Display` and fails when a +rendered statement no longer parses. + ClusterFuzzLite runs continuous fuzzing. Every pull request fuzzes for 10 minutes in `code-change` mode, a daily batch job grows the shared corpus stored on the `clusterfuzzlite` branch, and a daily prune compacts it. diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 4ee8c504d..ed69281a1 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -38,3 +38,10 @@ path = "fuzz_targets/fuzz_parse_sql.rs" test = false doc = false bench = false + +[[bin]] +name = "fuzz_parse_roundtrip" +path = "fuzz_targets/fuzz_parse_roundtrip.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/fuzz_parse_roundtrip.rs b/fuzz/fuzz_targets/fuzz_parse_roundtrip.rs new file mode 100644 index 000000000..f1b27d654 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_parse_roundtrip.rs @@ -0,0 +1,61 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 libfuzzer_sys::fuzz_target; +use sqlparser::dialect::{ + AnsiDialect, BigQueryDialect, ClickHouseDialect, DatabricksDialect, Dialect, DuckDbDialect, + GenericDialect, HiveDialect, MsSqlDialect, MySqlDialect, OracleDialect, PostgreSqlDialect, + RedshiftSqlDialect, SQLiteDialect, SnowflakeDialect, SparkSqlDialect, TeradataDialect, +}; +use sqlparser::parser::Parser; + +fuzz_target!(|sql: &str| { + let dialects: [(&str, &dyn Dialect); 16] = [ + ("ansi", &AnsiDialect {}), + ("bigquery", &BigQueryDialect {}), + ("clickhouse", &ClickHouseDialect {}), + ("databricks", &DatabricksDialect {}), + ("duckdb", &DuckDbDialect {}), + ("generic", &GenericDialect {}), + ("hive", &HiveDialect {}), + ("mssql", &MsSqlDialect {}), + ("mysql", &MySqlDialect {}), + ("oracle", &OracleDialect {}), + ("postgres", &PostgreSqlDialect {}), + ("redshift", &RedshiftSqlDialect {}), + ("sqlite", &SQLiteDialect {}), + ("snowflake", &SnowflakeDialect {}), + ("spark", &SparkSqlDialect {}), + ("teradata", &TeradataDialect {}), + ]; + for (name, dialect) in dialects { + let Ok(statements) = Parser::parse_sql(dialect, sql) else { + continue; + }; + for statement in &statements { + let rendered = statement.to_string(); + // SQL the AST renders must parse back. A failure is a Display bug + if let Err(err) = Parser::parse_sql(dialect, &rendered) { + panic!( + "{name}: displayed SQL failed to re-parse\n display: {rendered}\n error: {err}" + ); + } + } + } +}); diff --git a/fuzz/fuzz_targets/fuzz_parse_sql.rs b/fuzz/fuzz_targets/fuzz_parse_sql.rs index 89c841842..cc5945598 100644 --- a/fuzz/fuzz_targets/fuzz_parse_sql.rs +++ b/fuzz/fuzz_targets/fuzz_parse_sql.rs @@ -21,11 +21,11 @@ use libfuzzer_sys::fuzz_target; use sqlparser::dialect::{ AnsiDialect, BigQueryDialect, ClickHouseDialect, DatabricksDialect, Dialect, DuckDbDialect, GenericDialect, HiveDialect, MsSqlDialect, MySqlDialect, OracleDialect, PostgreSqlDialect, - RedshiftSqlDialect, SQLiteDialect, SnowflakeDialect, + RedshiftSqlDialect, SQLiteDialect, SnowflakeDialect, SparkSqlDialect, TeradataDialect, }; use sqlparser::parser::Parser; fuzz_target!(|sql: &str| { - let dialects: [&dyn Dialect; 14] = [ + let dialects: [&dyn Dialect; 16] = [ &AnsiDialect {}, &BigQueryDialect {}, &ClickHouseDialect {}, @@ -40,6 +40,8 @@ fuzz_target!(|sql: &str| { &RedshiftSqlDialect {}, &SQLiteDialect {}, &SnowflakeDialect {}, + &SparkSqlDialect {}, + &TeradataDialect {}, ]; for dialect in dialects { let _ = Parser::parse_sql(dialect, sql);