risingwave_sqlsmith/sqlreduce/passes/
replace.rs

1// Copyright 2025 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use risingwave_sqlparser::ast::{Expr, SelectItem, SetExpr, Value};
16
17use crate::sqlreduce::passes::{Ast, Transform, extract_query, extract_query_mut};
18
19/// Replace scalar constants in SELECT projections with canonical placeholders.
20///
21/// This transformation replaces literal values (e.g., numbers, strings, booleans)
22/// in the projection list with fixed placeholders. It simplifies the query while
23/// preserving structure, which is useful for debugging or failure reduction.
24///
25/// Replacement rules:
26/// - Number → 1
27/// - String → 'a'
28/// - Boolean → true
29/// - Null → remains null
30/// - Other values → replaced with null
31///
32/// Example:
33/// ```sql
34/// SELECT 42, 'hello', false, null;
35/// ```
36/// Will be reduced to:
37/// ```sql
38/// SELECT 1, 'a', true, null;
39/// ```
40pub struct ScalarReplace;
41
42impl ScalarReplace {
43    fn replacement_for_value(v: &Value) -> Value {
44        match v {
45            Value::Number(_) => Value::Number("1".to_owned()),
46            Value::Boolean(_) => Value::Boolean(true),
47            Value::SingleQuotedString(_) => Value::SingleQuotedString("a".to_owned()),
48            Value::Null => Value::Null,
49            _ => Value::Null,
50        }
51    }
52}
53
54impl Transform for ScalarReplace {
55    fn name(&self) -> String {
56        "scalar_replace".to_owned()
57    }
58
59    fn get_reduction_points(&self, ast: Ast) -> Vec<usize> {
60        let mut reduction_points = Vec::new();
61        if let Some(query) = extract_query(&ast)
62            && let SetExpr::Select(select) = &query.body
63        {
64            for (i, item) in select.projection.iter().enumerate() {
65                if let SelectItem::UnnamedExpr(Expr::Value(_)) = item {
66                    reduction_points.push(i);
67                }
68            }
69        }
70        reduction_points
71    }
72
73    fn apply_on(&self, ast: &mut Ast, reduction_points: Vec<usize>) -> Ast {
74        if let Some(query) = extract_query_mut(ast)
75            && let SetExpr::Select(select) = &mut query.body
76        {
77            for i in reduction_points {
78                if let SelectItem::UnnamedExpr(Expr::Value(ref mut v)) = select.projection[i] {
79                    let new_v = Self::replacement_for_value(v);
80                    *v = new_v;
81                }
82            }
83        }
84        ast.clone()
85    }
86}
87
88/// Replace all expressions in SELECT projections with NULL.
89///
90/// This transformation simplifies a query by replacing each unnamed expression
91/// in the SELECT list with `NULL`. It helps isolate whether the structure or
92/// presence of an expression is causing a failure, rather than its actual value.
93///
94/// Example:
95/// ```sql
96/// SELECT a + b, 42, 'hello';
97/// ```
98/// Will be reduced to:
99/// ```sql
100/// SELECT NULL, NULL, NULL;
101/// ```
102///
103/// Note: This transformation discards all computation and semantic meaning
104/// of expressions in the projection. It is intended purely for structural minimization.
105pub struct NullReplace;
106
107impl Transform for NullReplace {
108    fn name(&self) -> String {
109        "null_replace".to_owned()
110    }
111
112    fn get_reduction_points(&self, ast: Ast) -> Vec<usize> {
113        let mut reduction_points = Vec::new();
114        if let Some(query) = extract_query(&ast)
115            && let SetExpr::Select(select) = &query.body
116        {
117            for (i, _) in select.projection.iter().enumerate() {
118                reduction_points.push(i);
119            }
120        }
121        reduction_points
122    }
123
124    fn apply_on(&self, ast: &mut Ast, reduction_points: Vec<usize>) -> Ast {
125        if let Some(query) = extract_query_mut(ast)
126            && let SetExpr::Select(select) = &mut query.body
127        {
128            for i in reduction_points {
129                select.projection[i] = SelectItem::UnnamedExpr(Expr::Value(Value::Null));
130            }
131        }
132        ast.clone()
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::parse_sql;
140
141    #[test]
142    fn test_scalar_replace() {
143        let sql = "SELECT 42, 'hello', false, null;";
144        let ast = parse_sql(sql);
145        let reduction_points = ScalarReplace.get_reduction_points(ast[0].clone());
146        assert_eq!(reduction_points, vec![0, 1, 2, 3]);
147
148        let new_ast = ScalarReplace.apply_on(&mut ast[0].clone(), reduction_points);
149        assert_eq!(new_ast, parse_sql("SELECT 1, 'a', true, null;")[0].clone());
150    }
151
152    #[test]
153    fn test_null_replace() {
154        let sql = "SELECT a, b, c;";
155        let ast = parse_sql(sql);
156        let reduction_points = NullReplace.get_reduction_points(ast[0].clone());
157        assert_eq!(reduction_points, vec![0, 1, 2]);
158
159        let new_ast = NullReplace.apply_on(&mut ast[0].clone(), reduction_points);
160        assert_eq!(new_ast, parse_sql("SELECT NULL, NULL, NULL;")[0].clone());
161    }
162}