Skip to content

Commit a92eace

Browse files
committed
readyset-sql-passes: Cap IN explosion
Prevent OOMs when collapsing parameterized IN predicates by rejecting queries whose expanded lookup-key count would exceed a fixed cap. The cap is estimated before exploding params. Just uses a `MAX_EXPLODED_IN_LOOKUP_KEYS` constant, currently set to 10,000. Such a query would probably die for other reasons, but at least it won't instantly OOM the server. Fixes: REA-6017 Release-Note-Core: Fixed potential OOMs by rejecting queries with parameterized IN predicates that would expand to too many lookup keys. AI-Use: level:3 Change-Id: I38f6470887697478134858eb171a2a176a6a6964 Reviewed-on: https://gerrit.readyset.name/c/readyset/+/11147 Reviewed-by: Jason Brown <jason.b@readyset.io> Tested-by: Buildkite CI
1 parent 1dfff6d commit a92eace

File tree

3 files changed

+260
-111
lines changed

3 files changed

+260
-111
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Regression test for preventing OOM from parameterized IN cartesian explosion.
2+
3+
statement ok
4+
CREATE TABLE t (a int, b int, c int);
5+
6+
onlyif readyset
7+
statement error: Too many keys.*IN clause expansion
8+
SELECT *
9+
FROM t
10+
WHERE a IN (
11+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
12+
)
13+
AND b IN (
14+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
15+
)
16+
AND c IN (
17+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
18+
);

query-generator/src/lib.rs

Lines changed: 117 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -2859,6 +2859,104 @@ impl<'a> IntoIterator for &'a Operations {
28592859
}
28602860
}
28612861

2862+
/// Prune a randomly generated set of query operations to avoid combinations we don't support (or
2863+
/// that are too expensive) in fuzz-generated queries.
2864+
fn prune_query_operations(ops: &mut Vec<QueryOperation>) {
2865+
// Don't generate an aggregate with distinct keyword or a plain distinct in the same query as a
2866+
// WHERE IN clause, since we don't support those queries (ENG-2942)
2867+
let mut distinct_found = false;
2868+
let mut in_parameter_found = false;
2869+
2870+
// Don't generate an OR filter in the same query as a parameter of any kind, since
2871+
// we don't support those queries (ENG-2976)
2872+
let mut parameter_found = false;
2873+
let mut or_filter_found = false;
2874+
2875+
// Only allow one window function per query
2876+
// Window functions can't be used with: ranges, aggregates, or group by
2877+
let mut window_function_found = false;
2878+
let mut range_parameter_found = false;
2879+
let mut aggregate_found = false;
2880+
2881+
ops.retain(|op| match op {
2882+
QueryOperation::ColumnAggregate(agg) if agg.is_distinct() => {
2883+
if in_parameter_found || window_function_found {
2884+
false
2885+
} else {
2886+
distinct_found = true;
2887+
aggregate_found = true;
2888+
true
2889+
}
2890+
}
2891+
QueryOperation::Distinct => {
2892+
if in_parameter_found {
2893+
false
2894+
} else {
2895+
distinct_found = true;
2896+
true
2897+
}
2898+
}
2899+
QueryOperation::InParameter { .. } => {
2900+
if distinct_found || or_filter_found || window_function_found || in_parameter_found {
2901+
false
2902+
} else {
2903+
in_parameter_found = true;
2904+
parameter_found = true;
2905+
true
2906+
}
2907+
}
2908+
QueryOperation::SingleParameter | QueryOperation::MultipleParameters => {
2909+
if or_filter_found {
2910+
false
2911+
} else {
2912+
parameter_found = true;
2913+
true
2914+
}
2915+
}
2916+
QueryOperation::RangeParameter | QueryOperation::MultipleRangeParameters => {
2917+
if window_function_found || or_filter_found {
2918+
false
2919+
} else {
2920+
range_parameter_found = true;
2921+
parameter_found = true;
2922+
true
2923+
}
2924+
}
2925+
QueryOperation::Filter(Filter {
2926+
extend_where_with: LogicalOp::Or,
2927+
..
2928+
}) => {
2929+
if parameter_found {
2930+
false
2931+
} else {
2932+
or_filter_found = true;
2933+
true
2934+
}
2935+
}
2936+
QueryOperation::ColumnAggregate(_) => {
2937+
if window_function_found {
2938+
false
2939+
} else {
2940+
aggregate_found = true;
2941+
true
2942+
}
2943+
}
2944+
QueryOperation::WindowFunction(_) => {
2945+
if window_function_found
2946+
|| range_parameter_found
2947+
|| aggregate_found
2948+
|| in_parameter_found
2949+
{
2950+
false
2951+
} else {
2952+
window_function_found = true;
2953+
true
2954+
}
2955+
}
2956+
_ => true,
2957+
});
2958+
}
2959+
28622960
impl Arbitrary for Operations {
28632961
type Parameters = <Vec<QueryOperation> as Arbitrary>::Parameters;
28642962

@@ -2867,101 +2965,7 @@ impl Arbitrary for Operations {
28672965
fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
28682966
any_with::<Vec<QueryOperation>>(args)
28692967
.prop_map(|mut ops| {
2870-
// Don't generate an aggregate with distinct keyword or a plain distinct
2871-
// in the same query as a WHERE IN clause,
2872-
// since we don't support those queries (ENG-2942)
2873-
let mut distinct_found = false;
2874-
let mut in_parameter_found = false;
2875-
2876-
// Don't generate an OR filter in the same query as a parameter of any kind, since
2877-
// we don't support those queries (ENG-2976)
2878-
let mut parameter_found = false;
2879-
let mut or_filter_found = false;
2880-
2881-
// Only allow one window function per query
2882-
// Window functions can't be used with: ranges, aggregates, or group by
2883-
let mut window_function_found = false;
2884-
let mut range_parameter_found = false;
2885-
let mut aggregate_found = false;
2886-
2887-
ops.retain(|op| match op {
2888-
QueryOperation::ColumnAggregate(agg) if agg.is_distinct() => {
2889-
if in_parameter_found || window_function_found {
2890-
false
2891-
} else {
2892-
distinct_found = true;
2893-
aggregate_found = true;
2894-
true
2895-
}
2896-
}
2897-
QueryOperation::Distinct => {
2898-
if in_parameter_found {
2899-
false
2900-
} else {
2901-
distinct_found = true;
2902-
true
2903-
}
2904-
}
2905-
QueryOperation::InParameter { .. } => {
2906-
if distinct_found || or_filter_found || window_function_found {
2907-
false
2908-
} else {
2909-
in_parameter_found = true;
2910-
parameter_found = true;
2911-
true
2912-
}
2913-
}
2914-
QueryOperation::SingleParameter | QueryOperation::MultipleParameters => {
2915-
if or_filter_found {
2916-
false
2917-
} else {
2918-
parameter_found = true;
2919-
true
2920-
}
2921-
}
2922-
QueryOperation::RangeParameter | QueryOperation::MultipleRangeParameters => {
2923-
if window_function_found || or_filter_found {
2924-
false
2925-
} else {
2926-
range_parameter_found = true;
2927-
parameter_found = true;
2928-
true
2929-
}
2930-
}
2931-
QueryOperation::Filter(Filter {
2932-
extend_where_with: LogicalOp::Or,
2933-
..
2934-
}) => {
2935-
if parameter_found {
2936-
false
2937-
} else {
2938-
or_filter_found = true;
2939-
true
2940-
}
2941-
}
2942-
QueryOperation::ColumnAggregate(_) => {
2943-
if window_function_found {
2944-
false
2945-
} else {
2946-
aggregate_found = true;
2947-
true
2948-
}
2949-
}
2950-
QueryOperation::WindowFunction(_) => {
2951-
if window_function_found
2952-
|| range_parameter_found
2953-
|| aggregate_found
2954-
|| in_parameter_found
2955-
{
2956-
false
2957-
} else {
2958-
window_function_found = true;
2959-
true
2960-
}
2961-
}
2962-
_ => true,
2963-
});
2964-
2968+
prune_query_operations(&mut ops);
29652969
Operations(ops)
29662970
})
29672971
.boxed()
@@ -3573,6 +3577,24 @@ mod tests {
35733577
}
35743578
}
35753579

3580+
#[test]
3581+
fn prunes_extra_in_parameters() {
3582+
let mut ops = vec![
3583+
QueryOperation::InParameter { num_values: 2 },
3584+
QueryOperation::InParameter { num_values: 3 },
3585+
QueryOperation::SingleParameter,
3586+
];
3587+
3588+
prune_query_operations(&mut ops);
3589+
3590+
assert_eq!(
3591+
ops.into_iter()
3592+
.filter(|op| matches!(op, QueryOperation::InParameter { .. }))
3593+
.count(),
3594+
1
3595+
);
3596+
}
3597+
35763598
mod parse_num_operations {
35773599
use super::*;
35783600

0 commit comments

Comments
 (0)