risingwave_stream/executor/match_recognize/
proto.rs1use risingwave_pb::stream_plan::match_recognize_pattern_node::Node;
19use risingwave_pb::stream_plan::match_recognize_quantifier::Kind;
20use risingwave_pb::stream_plan::{MatchRecognizePatternNode, MatchRecognizeQuantifier};
21
22use super::nfa::{Pattern, Quantifier};
23
24const MAX_PERMUTE_VARS: usize = 6;
29
30const MAX_QUANTIFIER_BOUND: u32 = 1000;
33
34const MAX_PATTERN_NFA_STATES: u64 = 100_000;
41
42fn estimate_nfa_states(pattern: &Pattern) -> u64 {
47 match pattern {
48 Pattern::Var(_) => 2,
49 Pattern::Permute(vars) => {
50 let n = vars.len() as u64;
51 let orderings = (1..=n)
52 .try_fold(1u64, |acc, i| acc.checked_mul(i))
53 .unwrap_or(u64::MAX);
54 orderings
55 .saturating_mul(n.saturating_mul(2))
56 .saturating_add(2)
57 }
58 Pattern::Concat(ps) => ps
59 .iter()
60 .map(estimate_nfa_states)
61 .fold(0u64, u64::saturating_add)
62 .max(1),
63 Pattern::Alt(ps) => ps
64 .iter()
65 .map(estimate_nfa_states)
66 .fold(2u64, u64::saturating_add),
67 Pattern::Quantified(inner, q, _) => {
68 let inner = estimate_nfa_states(inner);
69 match q {
70 Quantifier::Star | Quantifier::Question => inner.saturating_add(2),
71 Quantifier::Plus => inner.saturating_mul(2).saturating_add(2),
72 Quantifier::Range { min, max } => {
75 let mandatory = inner.saturating_mul(u64::from(*min));
76 match max {
77 None => mandatory.saturating_add(inner).saturating_add(2),
78 Some(max) => {
79 let optional = inner
85 .saturating_add(2)
86 .saturating_mul(u64::from(max.saturating_sub(*min)));
87 mandatory.saturating_add(optional).max(1)
88 }
89 }
90 }
91 }
92 }
93 }
94}
95
96pub fn pattern_from_protobuf(pb: &MatchRecognizePatternNode) -> Result<Pattern, String> {
100 let pattern = decode_pattern(pb)?;
101 let states = estimate_nfa_states(&pattern);
102 if states > MAX_PATTERN_NFA_STATES {
103 return Err(format!(
104 "the pattern expands to an estimated {states} NFA states, above the supported \
105 maximum of {MAX_PATTERN_NFA_STATES}"
106 ));
107 }
108 Ok(pattern)
109}
110
111fn decode_pattern(pb: &MatchRecognizePatternNode) -> Result<Pattern, String> {
112 let node = pb
113 .node
114 .as_ref()
115 .ok_or_else(|| "empty MATCH_RECOGNIZE pattern node".to_owned())?;
116 Ok(match node {
117 Node::Var(v) => Pattern::Var(v.clone()),
118 Node::Concat(seq) => Pattern::Concat(patterns_from_protobuf(&seq.patterns)?),
119 Node::Alternation(seq) => Pattern::Alt(patterns_from_protobuf(&seq.patterns)?),
120 Node::Permute(p) => {
121 if p.vars.len() > MAX_PERMUTE_VARS {
122 return Err(format!(
123 "PERMUTE over {} variables exceeds the supported maximum of {}",
124 p.vars.len(),
125 MAX_PERMUTE_VARS
126 ));
127 }
128 Pattern::Permute(p.vars.clone())
129 }
130 Node::Quantified(q) => {
131 let inner = q
132 .inner
133 .as_ref()
134 .ok_or_else(|| "quantified pattern missing inner".to_owned())?;
135 let quantifier = quantifier_from_protobuf(
136 q.quantifier
137 .as_ref()
138 .ok_or_else(|| "quantified pattern missing quantifier".to_owned())?,
139 )?;
140 Pattern::Quantified(Box::new(decode_pattern(inner)?), quantifier, q.reluctant)
141 }
142 })
143}
144
145fn patterns_from_protobuf(patterns: &[MatchRecognizePatternNode]) -> Result<Vec<Pattern>, String> {
146 patterns.iter().map(decode_pattern).collect()
147}
148
149fn quantifier_from_protobuf(q: &MatchRecognizeQuantifier) -> Result<Quantifier, String> {
150 Ok(match q.kind() {
151 Kind::Star => Quantifier::Star,
152 Kind::Plus => Quantifier::Plus,
153 Kind::Question => Quantifier::Question,
154 Kind::Range => {
155 if q.min > MAX_QUANTIFIER_BOUND || q.max.is_some_and(|m| m > MAX_QUANTIFIER_BOUND) {
156 return Err(format!(
157 "quantifier bound {{{},{:?}}} exceeds the supported maximum of {}",
158 q.min, q.max, MAX_QUANTIFIER_BOUND
159 ));
160 }
161 if q.max.is_some_and(|m| m < q.min) {
164 return Err(format!(
165 "quantifier bound {{{},{:?}}} has max < min",
166 q.min, q.max
167 ));
168 }
169 Quantifier::Range {
170 min: q.min,
171 max: q.max,
172 }
173 }
174 Kind::Unspecified => {
175 return Err("unspecified MATCH_RECOGNIZE quantifier kind".to_owned());
176 }
177 })
178}
179
180#[cfg(test)]
181mod tests {
182
183 #[test]
188 fn an_oversized_whole_pattern_is_rejected_even_when_every_construct_is_in_bounds() {
189 let inner = quantified(var("a"), Kind::Range, 1000, Some(1000), false);
190 let nested = quantified(inner, Kind::Range, 1000, Some(1000), false);
191 let err = pattern_from_protobuf(&nested).expect_err("must not decode");
192 assert!(
193 err.contains("states"),
194 "the error should name the state expansion, got: {err}"
195 );
196 }
197
198 #[test]
208 fn a_bounded_range_counts_the_state_pair_each_optional_copy_allocates() {
209 let inner = quantified(var("a"), Kind::Range, 0, Some(1000), false);
210 let nested = quantified(inner, Kind::Range, 0, Some(50), false);
211 assert!(
212 pattern_from_protobuf(&nested).is_err(),
213 "must be rejected: this compiles to ~200k states, twice the cap"
214 );
215 }
216
217 #[test]
218 fn an_ordinary_pattern_is_well_under_the_state_cap() {
219 let pat = concat(vec![
220 var("d"),
221 quantified(var("b"), Kind::Star, 0, None, false),
222 var("w"),
223 ]);
224 assert!(pattern_from_protobuf(&pat).is_ok());
225 }
226 use risingwave_pb::stream_plan::match_recognize_pattern_node::Node;
227 use risingwave_pb::stream_plan::{
228 MatchRecognizePatternNode, MatchRecognizePatternSeq, MatchRecognizePermutePattern,
229 MatchRecognizeQuantifiedPattern, MatchRecognizeQuantifier,
230 };
231
232 use super::*;
233
234 fn var(name: &str) -> MatchRecognizePatternNode {
235 MatchRecognizePatternNode {
236 node: Some(Node::Var(name.to_owned())),
237 }
238 }
239
240 fn quantifier(kind: Kind, min: u32, max: Option<u32>) -> MatchRecognizeQuantifier {
241 MatchRecognizeQuantifier {
242 kind: kind as i32,
243 min,
244 max,
245 }
246 }
247
248 fn quantified(
249 inner: MatchRecognizePatternNode,
250 kind: Kind,
251 min: u32,
252 max: Option<u32>,
253 reluctant: bool,
254 ) -> MatchRecognizePatternNode {
255 MatchRecognizePatternNode {
256 node: Some(Node::Quantified(Box::new(
257 MatchRecognizeQuantifiedPattern {
258 inner: Some(Box::new(inner)),
259 quantifier: Some(quantifier(kind, min, max)),
260 reluctant,
261 },
262 ))),
263 }
264 }
265
266 fn concat(patterns: Vec<MatchRecognizePatternNode>) -> MatchRecognizePatternNode {
267 MatchRecognizePatternNode {
268 node: Some(Node::Concat(MatchRecognizePatternSeq { patterns })),
269 }
270 }
271
272 fn alt(patterns: Vec<MatchRecognizePatternNode>) -> MatchRecognizePatternNode {
273 MatchRecognizePatternNode {
274 node: Some(Node::Alternation(MatchRecognizePatternSeq { patterns })),
275 }
276 }
277
278 #[test]
279 fn decode_concat() {
280 assert_eq!(
281 pattern_from_protobuf(&concat(vec![var("a"), var("b"), var("c")])).unwrap(),
282 Pattern::Concat(vec![
283 Pattern::Var("a".to_owned()),
284 Pattern::Var("b".to_owned()),
285 Pattern::Var("c".to_owned()),
286 ])
287 );
288 }
289
290 #[test]
291 fn decode_quantifiers() {
292 assert_eq!(
293 pattern_from_protobuf(&concat(vec![
294 var("a"),
295 quantified(var("b"), Kind::Plus, 0, None, false),
296 quantified(var("c"), Kind::Question, 0, None, false),
297 ]))
298 .unwrap(),
299 Pattern::Concat(vec![
300 Pattern::Var("a".to_owned()),
301 Pattern::Quantified(
302 Box::new(Pattern::Var("b".to_owned())),
303 Quantifier::Plus,
304 false
305 ),
306 Pattern::Quantified(
307 Box::new(Pattern::Var("c".to_owned())),
308 Quantifier::Question,
309 false
310 ),
311 ])
312 );
313 assert_eq!(
314 pattern_from_protobuf(&quantified(var("a"), Kind::Star, 0, None, true)).unwrap(),
315 Pattern::Quantified(
316 Box::new(Pattern::Var("a".to_owned())),
317 Quantifier::Star,
318 true
319 )
320 );
321 }
322
323 #[test]
324 fn decode_alternation_and_range() {
325 assert_eq!(
326 pattern_from_protobuf(&concat(vec![
327 alt(vec![var("a"), var("b")]),
328 quantified(var("c"), Kind::Range, 1, Some(3), false),
329 ]))
330 .unwrap(),
331 Pattern::Concat(vec![
332 Pattern::Alt(vec![
333 Pattern::Var("a".to_owned()),
334 Pattern::Var("b".to_owned())
335 ]),
336 Pattern::Quantified(
337 Box::new(Pattern::Var("c".to_owned())),
338 Quantifier::Range {
339 min: 1,
340 max: Some(3)
341 },
342 false
343 ),
344 ])
345 );
346 }
347
348 #[test]
349 fn decode_permute() {
350 assert_eq!(
351 pattern_from_protobuf(&MatchRecognizePatternNode {
352 node: Some(Node::Permute(MatchRecognizePermutePattern {
353 vars: vec!["a".to_owned(), "b".to_owned(), "c".to_owned()],
354 })),
355 })
356 .unwrap(),
357 Pattern::Permute(vec!["a".to_owned(), "b".to_owned(), "c".to_owned()])
358 );
359 }
360
361 #[test]
362 fn rejects_empty_node() {
363 assert!(pattern_from_protobuf(&MatchRecognizePatternNode { node: None }).is_err());
364 }
365
366 #[test]
369 fn rejects_oversized_permute_and_range() {
370 let vars: Vec<String> = (0..7).map(|i| format!("v{i}")).collect();
371 assert!(
372 pattern_from_protobuf(&MatchRecognizePatternNode {
373 node: Some(Node::Permute(MatchRecognizePermutePattern { vars })),
374 })
375 .unwrap_err()
376 .contains("PERMUTE")
377 );
378 assert!(
379 pattern_from_protobuf(&quantified(
380 var("a"),
381 Kind::Range,
382 4_000_000_000,
383 None,
384 false
385 ))
386 .unwrap_err()
387 .contains("quantifier bound")
388 );
389 assert!(
390 pattern_from_protobuf(&quantified(
391 var("a"),
392 Kind::Range,
393 1,
394 Some(4_000_000_000),
395 false
396 ))
397 .unwrap_err()
398 .contains("quantifier bound")
399 );
400 }
401}