| 
 | 1 | +// Licensed to the Apache Software Foundation (ASF) under one  | 
 | 2 | +// or more contributor license agreements.  See the NOTICE file  | 
 | 3 | +// distributed with this work for additional information  | 
 | 4 | +// regarding copyright ownership.  The ASF licenses this file  | 
 | 5 | +// to you under the Apache License, Version 2.0 (the  | 
 | 6 | +// "License"); you may not use this file except in compliance  | 
 | 7 | +// with the License.  You may obtain a copy of the License at  | 
 | 8 | +//  | 
 | 9 | +//   http://www.apache.org/licenses/LICENSE-2.0  | 
 | 10 | +//  | 
 | 11 | +// Unless required by applicable law or agreed to in writing,  | 
 | 12 | +// software distributed under the License is distributed on an  | 
 | 13 | +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY  | 
 | 14 | +// KIND, either express or implied.  See the License for the  | 
 | 15 | +// specific language governing permissions and limitations  | 
 | 16 | +// under the License.  | 
 | 17 | + | 
 | 18 | +use datafusion::sql::parser::{DFParserBuilder, Statement};  | 
 | 19 | +use sqllogictest::{AsyncDB, Record};  | 
 | 20 | +use sqlparser::ast::{SetExpr, Statement as SqlStatement};  | 
 | 21 | +use sqlparser::dialect::dialect_from_str;  | 
 | 22 | +use std::path::Path;  | 
 | 23 | +use std::str::FromStr;  | 
 | 24 | + | 
 | 25 | +/// Filter specification that determines whether a certain sqllogictest record in  | 
 | 26 | +/// a certain file should be filtered. In order for a [`Filter`] to match a test case:  | 
 | 27 | +///  | 
 | 28 | +/// - The test must belong to a file whose absolute path contains the `file_substring` substring.  | 
 | 29 | +/// - If a `line_number` is specified, the test must be declared in that same line number.  | 
 | 30 | +///  | 
 | 31 | +/// If a [`Filter`] matches a specific test case, then the record is executed, if there's  | 
 | 32 | +/// no match, the record is skipped.  | 
 | 33 | +///  | 
 | 34 | +/// Filters can be parsed from strings of the form `<file_name>:line_number`. For example,  | 
 | 35 | +/// `foo.slt:100` matches any test whose name contains `foo.slt` and the test starts on line  | 
 | 36 | +/// number 100.  | 
 | 37 | +#[derive(Debug, Clone)]  | 
 | 38 | +pub struct Filter {  | 
 | 39 | +    file_substring: String,  | 
 | 40 | +    line_number: Option<u32>,  | 
 | 41 | +}  | 
 | 42 | + | 
 | 43 | +impl FromStr for Filter {  | 
 | 44 | +    type Err = String;  | 
 | 45 | + | 
 | 46 | +    fn from_str(s: &str) -> Result<Self, Self::Err> {  | 
 | 47 | +        let parts: Vec<&str> = s.rsplitn(2, ':').collect();  | 
 | 48 | +        if parts.len() == 2 {  | 
 | 49 | +            match parts[0].parse::<u32>() {  | 
 | 50 | +                Ok(line) => Ok(Filter {  | 
 | 51 | +                    file_substring: parts[1].to_string(),  | 
 | 52 | +                    line_number: Some(line),  | 
 | 53 | +                }),  | 
 | 54 | +                Err(_) => Err(format!("Cannot parse line number from '{s}'")),  | 
 | 55 | +            }  | 
 | 56 | +        } else {  | 
 | 57 | +            Ok(Filter {  | 
 | 58 | +                file_substring: s.to_string(),  | 
 | 59 | +                line_number: None,  | 
 | 60 | +            })  | 
 | 61 | +        }  | 
 | 62 | +    }  | 
 | 63 | +}  | 
 | 64 | + | 
 | 65 | +/// Given a list of [`Filter`]s, determines if the whole file in the provided  | 
 | 66 | +/// path can be skipped.  | 
 | 67 | +///  | 
 | 68 | +/// - If there's at least 1 filter whose file name is a substring of the provided path,  | 
 | 69 | +///   it returns true.  | 
 | 70 | +/// - If the provided filter list is empty, it returns false.  | 
 | 71 | +pub fn should_skip_file(path: &Path, filters: &[Filter]) -> bool {  | 
 | 72 | +    if filters.is_empty() {  | 
 | 73 | +        return false;  | 
 | 74 | +    }  | 
 | 75 | + | 
 | 76 | +    let path_string = path.to_string_lossy();  | 
 | 77 | +    for filter in filters {  | 
 | 78 | +        if path_string.contains(&filter.file_substring) {  | 
 | 79 | +            return false;  | 
 | 80 | +        }  | 
 | 81 | +    }  | 
 | 82 | +    true  | 
 | 83 | +}  | 
 | 84 | + | 
 | 85 | +/// Determines whether a certain sqllogictest record should be skipped given the provided  | 
 | 86 | +/// filters.  | 
 | 87 | +///  | 
 | 88 | +/// If there's at least 1 matching filter, or the filter list is empty, it returns false.  | 
 | 89 | +///  | 
 | 90 | +/// There are certain records that will never be skipped even if they are not matched  | 
 | 91 | +/// by any filters, like CREATE TABLE, INSERT INTO, DROP or SELECT * INTO statements,  | 
 | 92 | +/// as they populate tables necessary for other tests to work.  | 
 | 93 | +pub fn should_skip_record<D: AsyncDB>(  | 
 | 94 | +    record: &Record<D::ColumnType>,  | 
 | 95 | +    filters: &[Filter],  | 
 | 96 | +) -> bool {  | 
 | 97 | +    if filters.is_empty() {  | 
 | 98 | +        return false;  | 
 | 99 | +    }  | 
 | 100 | + | 
 | 101 | +    let (sql, loc) = match record {  | 
 | 102 | +        Record::Statement { sql, loc, .. } => (sql, loc),  | 
 | 103 | +        Record::Query { sql, loc, .. } => (sql, loc),  | 
 | 104 | +        _ => return false,  | 
 | 105 | +    };  | 
 | 106 | + | 
 | 107 | +    let statement = if let Some(statement) = parse_or_none(sql, "Postgres") {  | 
 | 108 | +        statement  | 
 | 109 | +    } else if let Some(statement) = parse_or_none(sql, "generic") {  | 
 | 110 | +        statement  | 
 | 111 | +    } else {  | 
 | 112 | +        return false;  | 
 | 113 | +    };  | 
 | 114 | + | 
 | 115 | +    if !statement_is_skippable(&statement) {  | 
 | 116 | +        return false;  | 
 | 117 | +    }  | 
 | 118 | + | 
 | 119 | +    for filter in filters {  | 
 | 120 | +        if !loc.file().contains(&filter.file_substring) {  | 
 | 121 | +            continue;  | 
 | 122 | +        }  | 
 | 123 | +        if let Some(line_num) = filter.line_number {  | 
 | 124 | +            if loc.line() != line_num {  | 
 | 125 | +                continue;  | 
 | 126 | +            }  | 
 | 127 | +        }  | 
 | 128 | + | 
 | 129 | +        // This filter matches both file name substring and the exact  | 
 | 130 | +        //  line number (if one was provided), so don't skip it.  | 
 | 131 | +        return false;  | 
 | 132 | +    }  | 
 | 133 | + | 
 | 134 | +    true  | 
 | 135 | +}  | 
 | 136 | + | 
 | 137 | +fn statement_is_skippable(statement: &Statement) -> bool {  | 
 | 138 | +    // Only SQL statements can be skipped.  | 
 | 139 | +    let Statement::Statement(sql_stmt) = statement else {  | 
 | 140 | +        return false;  | 
 | 141 | +    };  | 
 | 142 | + | 
 | 143 | +    // Cannot skip SELECT INTO statements, as they can also create tables  | 
 | 144 | +    // that further test cases will use.  | 
 | 145 | +    if let SqlStatement::Query(v) = sql_stmt.as_ref() {  | 
 | 146 | +        if let SetExpr::Select(v) = v.body.as_ref() {  | 
 | 147 | +            if v.into.is_some() {  | 
 | 148 | +                return false;  | 
 | 149 | +            }  | 
 | 150 | +        }  | 
 | 151 | +    }  | 
 | 152 | + | 
 | 153 | +    // Only SELECT and EXPLAIN statements can be skipped, as any other  | 
 | 154 | +    // statement might be populating tables that future test cases will use.  | 
 | 155 | +    matches!(  | 
 | 156 | +        sql_stmt.as_ref(),  | 
 | 157 | +        SqlStatement::Query(_) | SqlStatement::Explain { .. }  | 
 | 158 | +    )  | 
 | 159 | +}  | 
 | 160 | + | 
 | 161 | +fn parse_or_none(sql: &str, dialect: &str) -> Option<Statement> {  | 
 | 162 | +    let Ok(Ok(Some(statement))) = DFParserBuilder::new(sql)  | 
 | 163 | +        .with_dialect(dialect_from_str(dialect).unwrap().as_ref())  | 
 | 164 | +        .build()  | 
 | 165 | +        .map(|mut v| v.parse_statements().map(|mut v| v.pop_front()))  | 
 | 166 | +    else {  | 
 | 167 | +        return None;  | 
 | 168 | +    };  | 
 | 169 | +    Some(statement)  | 
 | 170 | +}  | 
0 commit comments