1use super::{to_f64, value_to_string, Result, SQLError, TemporalValue, Value};
10
11pub(super) fn typeof_value(v: &Value) -> String {
16 match v {
17 Value::Null => "null".into(),
18 Value::Bool(_) => "boolean".into(),
19 Value::Int(_) => "integer".into(),
20 Value::Float(_) => "double precision".into(),
21 Value::Decimal(_) => "numeric".into(),
22 Value::Str(_) => "text".into(),
23 Value::FixedChar(_) => "character".into(),
24 Value::Bytes(_) => "bytea".into(),
25 Value::Temporal(value) => match value {
26 TemporalValue::Date { .. } => "date".into(),
27 TemporalValue::Time { .. } => "time without time zone".into(),
28 TemporalValue::TimeTz { .. } => "time with time zone".into(),
29 TemporalValue::Timestamp { .. } => "timestamp without time zone".into(),
30 TemporalValue::TimestampTz { .. } => "timestamp with time zone".into(),
31 TemporalValue::Interval { .. } => "interval".into(),
32 },
33 Value::Json(_) => "json".into(),
34 Value::JsonB(_) => "jsonb".into(),
35 Value::Array(_) => "array".into(),
36 Value::List(_) => "array".into(),
37 Value::Row(_) | Value::Record(_) => "record".into(),
38 Value::Map(_) => "jsonb".into(),
39 }
40}
41
42pub(super) fn point_xy(v: &Value) -> Result<(f64, f64)> {
43 match v {
44 Value::List(items) if items.len() == 2 => Ok((to_f64(&items[0])?, to_f64(&items[1])?)),
45 Value::Str(s) | Value::FixedChar(s) => {
46 let cleaned = s.trim_matches(|c: char| c == '(' || c == ')' || c == '[' || c == ']');
47 let parts: Vec<&str> = cleaned.split(',').map(str::trim).collect();
48 if parts.len() != 2 {
49 return Err(SQLError::TypeMismatch(format!("point: cannot parse {s:?}")));
50 }
51 let x: f64 = parts[0]
52 .parse()
53 .map_err(|e| SQLError::TypeMismatch(format!("point.x: {e}")))?;
54 let y: f64 = parts[1]
55 .parse()
56 .map_err(|e| SQLError::TypeMismatch(format!("point.y: {e}")))?;
57 Ok((x, y))
58 }
59 other => Err(SQLError::TypeMismatch(format!(
60 "point: not coercible {other:?}"
61 ))),
62 }
63}
64
65pub struct CompiledLikePattern {
70 case_insensitive: bool,
71 pattern_chars: Vec<char>,
72 pattern_ascii: Option<Vec<u8>>,
73}
74
75impl CompiledLikePattern {
76 #[must_use]
77 pub fn new(pattern: &str, case_insensitive: bool) -> Self {
78 let normalized = if case_insensitive {
79 pattern.to_lowercase()
80 } else {
81 pattern.to_string()
82 };
83 let pattern_ascii = normalized
84 .is_ascii()
85 .then(|| normalized.as_bytes().to_vec());
86 let pattern_chars = normalized.chars().collect();
87 Self {
88 case_insensitive,
89 pattern_chars,
90 pattern_ascii,
91 }
92 }
93
94 #[must_use]
95 pub fn from_value(pattern: &Value, case_insensitive: bool) -> Self {
96 Self::new(&value_to_string(pattern), case_insensitive)
97 }
98
99 #[must_use]
100 pub fn is_match(&self, haystack: &str) -> bool {
101 if self.case_insensitive {
102 let normalized = haystack.to_lowercase();
103 if let Some(pattern) = self
104 .pattern_ascii
105 .as_deref()
106 .filter(|_| normalized.is_ascii())
107 {
108 return wildcard_match(normalized.as_bytes(), pattern, b'%', b'_');
109 }
110 let haystack = normalized.chars().collect::<Vec<_>>();
111 return wildcard_match(&haystack, &self.pattern_chars, '%', '_');
112 }
113 if let Some(pattern) = self
114 .pattern_ascii
115 .as_deref()
116 .filter(|_| haystack.is_ascii())
117 {
118 return wildcard_match(haystack.as_bytes(), pattern, b'%', b'_');
119 }
120 let haystack = haystack.chars().collect::<Vec<_>>();
121 wildcard_match(&haystack, &self.pattern_chars, '%', '_')
122 }
123
124 #[must_use]
125 pub fn matches_value(&self, haystack: &Value) -> bool {
126 match haystack {
127 Value::Str(text) => self.is_match(text),
128 Value::FixedChar(text) => self.is_match(text.trim_end_matches(' ')),
129 Value::Null => self.is_match(""),
130 other => self.is_match(&value_to_string(other)),
131 }
132 }
133}
134
135fn wildcard_match<T: Copy + Eq>(
136 haystack: &[T],
137 pattern: &[T],
138 wildcard_many: T,
139 wildcard_one: T,
140) -> bool {
141 let mut haystack_index = 0;
142 let mut pattern_index = 0;
143 let mut star: Option<(usize, usize)> = None;
144 while haystack_index < haystack.len() {
145 if pattern_index < pattern.len()
146 && (pattern[pattern_index] == wildcard_one
147 || pattern[pattern_index] == haystack[haystack_index])
148 {
149 haystack_index += 1;
150 pattern_index += 1;
151 } else if pattern_index < pattern.len() && pattern[pattern_index] == wildcard_many {
152 star = Some((pattern_index, haystack_index));
153 pattern_index += 1;
154 } else if let Some((star_pattern, star_haystack)) = star {
155 pattern_index = star_pattern + 1;
156 haystack_index = star_haystack + 1;
157 star = Some((star_pattern, star_haystack + 1));
158 } else {
159 return false;
160 }
161 }
162 while pattern_index < pattern.len() && pattern[pattern_index] == wildcard_many {
163 pattern_index += 1;
164 }
165 pattern_index == pattern.len()
166}
167
168pub(super) fn like_match(haystack: &str, pattern: &str, case_insensitive: bool) -> bool {
169 CompiledLikePattern::new(pattern, case_insensitive).is_match(haystack)
170}
171
172pub(super) fn trim_chars(args: &[Value], start: bool, end: bool) -> Result<Value> {
175 if args.is_empty() || args.len() > 2 {
176 return Err(SQLError::TypeMismatch("trim takes 1-2 args".into()));
177 }
178 if args.iter().any(|arg| matches!(arg, Value::Null)) {
179 return Ok(Value::Null);
180 }
181 let s = value_to_string(&args[0]);
182 let out = match args.get(1) {
183 None => match (start, end) {
184 (true, true) => s.trim(),
185 (true, false) => s.trim_start(),
186 (false, true) => s.trim_end(),
187 (false, false) => s.as_str(),
188 }
189 .to_string(),
190 Some(set) => {
191 let set: Vec<char> = value_to_string(set).chars().collect();
192 let matches_set = |c: char| set.contains(&c);
193 let mut out = s.as_str();
194 if start {
195 out = out.trim_start_matches(matches_set);
196 }
197 if end {
198 out = out.trim_end_matches(matches_set);
199 }
200 out.to_string()
201 }
202 };
203 Ok(Value::Str(out))
204}
205
206pub(super) fn compile_pg_regex(
208 pattern: &str,
209 flags: &str,
210 global_allowed: bool,
211) -> Result<regex::Regex> {
212 #[derive(Clone, Copy)]
213 enum Syntax {
214 Advanced,
215 Basic,
216 Quoted,
217 }
218
219 let mut case_insensitive = false;
220 let mut multi_line = false;
221 let mut dot_matches_new_line = true;
222 let mut expanded = false;
223 let mut syntax = Syntax::Advanced;
224 for flag in flags.chars() {
225 match flag {
226 'g' if global_allowed => {}
227 'b' | 'e' => syntax = Syntax::Basic,
231 'c' => case_insensitive = false,
232 'i' => case_insensitive = true,
233 'm' | 'n' => {
234 multi_line = true;
235 dot_matches_new_line = false;
236 }
237 'p' => {
238 multi_line = false;
239 dot_matches_new_line = false;
240 }
241 'q' => syntax = Syntax::Quoted,
242 's' => {
243 multi_line = false;
244 dot_matches_new_line = true;
245 }
246 't' => expanded = false,
247 'w' => {
248 multi_line = true;
249 dot_matches_new_line = true;
250 }
251 'x' => expanded = true,
252 invalid => {
253 return Err(SQLError::Routine {
254 sqlstate: "22023".into(),
255 message: format!("invalid regular expression option: \"{invalid}\""),
256 });
257 }
258 }
259 }
260 if matches!(syntax, Syntax::Quoted) && (expanded || multi_line || !dot_matches_new_line) {
261 return Err(SQLError::Routine {
262 sqlstate: "2201B".into(),
263 message: "invalid regular expression: invalid argument to regex function".into(),
264 });
265 }
266 let pattern = if expanded {
267 expand_postgres_regex(pattern)
268 } else {
269 pattern.to_string()
270 };
271 let pattern = match syntax {
272 Syntax::Advanced => pattern,
273 Syntax::Basic => postgres_basic_regex(&pattern),
274 Syntax::Quoted => regex::escape(&pattern),
275 };
276 let pattern = postgres_character_class_regex(&pattern, !dot_matches_new_line);
277 let mut builder = regex::RegexBuilder::new(&pattern);
278 builder
279 .case_insensitive(case_insensitive)
280 .multi_line(multi_line)
281 .dot_matches_new_line(dot_matches_new_line);
282 builder.build().map_err(|error| SQLError::Routine {
283 sqlstate: "2201B".into(),
284 message: format!("invalid regular expression: {error}"),
285 })
286}
287
288fn postgres_character_class_regex(pattern: &str, exclude_newline: bool) -> String {
289 let characters = pattern.chars().collect::<Vec<_>>();
290 let mut output = String::with_capacity(pattern.len());
291 let mut position = 0usize;
292 let mut in_bracket = false;
293 let mut bracket_can_close = false;
294 while let Some(&character) = characters.get(position) {
295 position += 1;
296 if character == '\\' {
297 output.push(character);
298 if let Some(&escaped) = characters.get(position) {
299 position += 1;
300 output.push(escaped);
301 if in_bracket {
302 bracket_can_close = true;
303 }
304 }
305 continue;
306 }
307 if !in_bracket {
308 output.push(character);
309 if character == '[' {
310 in_bracket = true;
311 bracket_can_close = false;
312 if characters.get(position) == Some(&'^') {
313 position += 1;
314 output.push('^');
315 if characters.get(position) == Some(&']') {
316 position += 1;
317 output.push(']');
318 bracket_can_close = true;
319 }
320 if exclude_newline {
321 output.push_str("\\n");
322 if characters.get(position) == Some(&'-') {
323 position += 1;
324 output.push_str("\\-");
325 bracket_can_close = true;
326 }
327 }
328 }
329 }
330 continue;
331 }
332 if character == '[' && matches!(characters.get(position), Some('.' | ':' | '=')) {
333 let delimiter = characters[position];
334 output.push(character);
335 output.push(delimiter);
336 position += 1;
337 while let Some(&nested) = characters.get(position) {
338 position += 1;
339 output.push(nested);
340 if nested == delimiter && characters.get(position) == Some(&']') {
341 output.push(']');
342 position += 1;
343 break;
344 }
345 }
346 bracket_can_close = true;
347 continue;
348 }
349 if character == '[' {
350 output.push_str("\\[");
351 bracket_can_close = true;
352 continue;
353 }
354 output.push(character);
355 if character == ']' && bracket_can_close {
356 in_bracket = false;
357 } else if character != '^' || bracket_can_close {
358 bracket_can_close = true;
359 }
360 }
361 output
362}
363
364fn expand_postgres_regex(pattern: &str) -> String {
365 let mut output = String::with_capacity(pattern.len());
366 let mut characters = pattern.chars().peekable();
367 let mut in_bracket = false;
368 let mut bracket_can_close = false;
369 while let Some(character) = characters.next() {
370 if character == '\\' {
371 output.push(character);
372 if let Some(escaped) = characters.next() {
373 output.push(escaped);
374 if in_bracket {
375 bracket_can_close = true;
376 }
377 }
378 continue;
379 }
380 if in_bracket {
381 if character == '[' {
382 if let Some(delimiter @ ('.' | ':' | '=')) = characters.peek().copied() {
383 output.push(character);
384 output.push(delimiter);
385 characters.next();
386 while let Some(nested) = characters.next() {
387 output.push(nested);
388 if nested == delimiter && characters.peek() == Some(&']') {
389 output.push(']');
390 characters.next();
391 break;
392 }
393 }
394 bracket_can_close = true;
395 continue;
396 }
397 }
398 output.push(character);
399 if character == ']' && bracket_can_close {
400 in_bracket = false;
401 } else if character != '^' || bracket_can_close {
402 bracket_can_close = true;
403 }
404 continue;
405 }
406 match character {
407 '[' => {
408 in_bracket = true;
409 bracket_can_close = false;
410 output.push(character);
411 }
412 '#' => {
413 for comment in characters.by_ref() {
414 if comment == '\n' {
415 break;
416 }
417 }
418 }
419 whitespace if postgres_expanded_regex_whitespace(whitespace) => {}
420 other => output.push(other),
421 }
422 }
423 output
424}
425
426fn postgres_expanded_regex_whitespace(character: char) -> bool {
427 matches!(
428 character,
429 '\u{0009}'..='\u{000D}'
430 | '\u{0020}'
431 | '\u{1680}'
432 | '\u{2000}'..='\u{2006}'
433 | '\u{2008}'..='\u{200A}'
434 | '\u{2028}'..='\u{2029}'
435 | '\u{205F}'
436 | '\u{3000}'
437 )
438}
439
440fn postgres_basic_regex(pattern: &str) -> String {
441 let mut output = String::with_capacity(pattern.len());
442 let characters = pattern.chars().collect::<Vec<_>>();
443 let mut position = 0usize;
444 let mut in_bracket = false;
445 let mut bracket_can_close = false;
446 let mut at_subexpression_start = true;
447 while let Some(&character) = characters.get(position) {
448 position += 1;
449 if in_bracket {
450 if character == '\\' {
451 output.push_str(r"\\");
452 bracket_can_close = true;
453 continue;
454 }
455 output.push(character);
456 if character == ']' && bracket_can_close {
457 in_bracket = false;
458 at_subexpression_start = false;
459 } else if character != '^' || bracket_can_close {
460 bracket_can_close = true;
461 }
462 continue;
463 }
464 if character == '\\' {
465 match characters.get(position).copied() {
466 Some('(') => {
467 position += 1;
468 output.push('(');
469 at_subexpression_start = true;
470 }
471 Some(')') => {
472 position += 1;
473 output.push(')');
474 at_subexpression_start = false;
475 }
476 Some(bound @ ('{' | '}')) => {
477 position += 1;
478 output.push(bound);
479 }
480 Some(escaped) if escaped.is_ascii_alphabetic() => {
481 position += 1;
482 output.push(escaped);
483 at_subexpression_start = false;
484 }
485 Some(escaped) => {
486 position += 1;
487 output.push('\\');
488 output.push(escaped);
489 at_subexpression_start = false;
490 }
491 None => output.push('\\'),
492 }
493 continue;
494 }
495 match character {
496 '[' => {
497 in_bracket = true;
498 bracket_can_close = false;
499 output.push(character);
500 }
501 '^' if at_subexpression_start => output.push(character),
502 '^' => {
503 output.push_str(r"\^");
504 at_subexpression_start = false;
505 }
506 '$' => {
507 let closes_subexpression = matches!(
508 (characters.get(position), characters.get(position + 1)),
509 (Some('\\'), Some(')'))
510 );
511 if position == characters.len() || closes_subexpression {
512 output.push(character);
513 } else {
514 output.push_str(r"\$");
515 at_subexpression_start = false;
516 }
517 }
518 '*' if at_subexpression_start => {
519 output.push_str(r"\*");
520 at_subexpression_start = false;
521 }
522 literal @ ('+' | '?' | '(' | ')' | '{' | '}' | '|') => {
523 output.push('\\');
524 output.push(literal);
525 at_subexpression_start = false;
526 }
527 other => {
528 output.push(other);
529 at_subexpression_start = false;
530 }
531 }
532 }
533 output
534}
535
536pub(super) fn is_quoted_keyword(word: &str) -> bool {
539 const KEYWORDS: &[&str] = &[
540 "all",
541 "analyse",
542 "analyze",
543 "and",
544 "any",
545 "array",
546 "as",
547 "asc",
548 "asymmetric",
549 "authorization",
550 "between",
551 "bigint",
552 "binary",
553 "bit",
554 "boolean",
555 "both",
556 "case",
557 "cast",
558 "char",
559 "character",
560 "check",
561 "coalesce",
562 "collate",
563 "collation",
564 "column",
565 "concurrently",
566 "constraint",
567 "create",
568 "cross",
569 "current_catalog",
570 "current_date",
571 "current_role",
572 "current_schema",
573 "current_time",
574 "current_timestamp",
575 "current_user",
576 "dec",
577 "decimal",
578 "default",
579 "deferrable",
580 "desc",
581 "distinct",
582 "do",
583 "else",
584 "end",
585 "except",
586 "exists",
587 "extract",
588 "false",
589 "fetch",
590 "float",
591 "for",
592 "foreign",
593 "freeze",
594 "from",
595 "full",
596 "grant",
597 "greatest",
598 "group",
599 "grouping",
600 "having",
601 "ilike",
602 "in",
603 "initially",
604 "inner",
605 "inout",
606 "int",
607 "integer",
608 "intersect",
609 "interval",
610 "into",
611 "is",
612 "isnull",
613 "join",
614 "json",
615 "json_array",
616 "json_arrayagg",
617 "json_exists",
618 "json_object",
619 "json_objectagg",
620 "json_query",
621 "json_scalar",
622 "json_serialize",
623 "json_table",
624 "json_value",
625 "lateral",
626 "leading",
627 "least",
628 "left",
629 "like",
630 "limit",
631 "localtime",
632 "localtimestamp",
633 "merge_action",
634 "national",
635 "natural",
636 "nchar",
637 "none",
638 "normalize",
639 "not",
640 "notnull",
641 "null",
642 "nullif",
643 "numeric",
644 "offset",
645 "on",
646 "only",
647 "or",
648 "order",
649 "out",
650 "outer",
651 "overlaps",
652 "overlay",
653 "placing",
654 "position",
655 "precision",
656 "primary",
657 "real",
658 "references",
659 "returning",
660 "right",
661 "row",
662 "select",
663 "session_user",
664 "setof",
665 "similar",
666 "smallint",
667 "some",
668 "substring",
669 "symmetric",
670 "system_user",
671 "table",
672 "tablesample",
673 "then",
674 "time",
675 "timestamp",
676 "to",
677 "trailing",
678 "treat",
679 "trim",
680 "true",
681 "union",
682 "unique",
683 "user",
684 "using",
685 "values",
686 "varchar",
687 "variadic",
688 "verbose",
689 "when",
690 "where",
691 "window",
692 "with",
693 "xmlattributes",
694 "xmlconcat",
695 "xmlelement",
696 "xmlexists",
697 "xmlforest",
698 "xmlnamespaces",
699 "xmlparse",
700 "xmlpi",
701 "xmlroot",
702 "xmlserialize",
703 "xmltable",
704 ];
705 KEYWORDS.binary_search(&word).is_ok()
706}
707
708pub fn quote_ident(ident: &str) -> String {
711 let safe = !ident.is_empty()
712 && ident.chars().enumerate().all(|(i, c)| {
713 c.is_ascii_lowercase() || c == '_' || (i > 0 && (c.is_ascii_digit() || c == '$'))
714 });
715 if safe && !is_quoted_keyword(ident) {
716 return ident.to_string();
717 }
718 format!("\"{}\"", ident.replace('"', "\"\""))
719}
720
721pub(super) fn quote_literal(text: &str) -> String {
724 let escaped = text.replace('\'', "''");
725 if escaped.contains('\\') {
726 format!("E'{}'", escaped.replace('\\', "\\\\"))
727 } else {
728 format!("'{escaped}'")
729 }
730}
731
732pub(super) fn similar_to_regex(pattern: &str) -> String {
736 let mut out = String::with_capacity(pattern.len() + 8);
737 out.push_str("^(?:");
738 let mut chars = pattern.chars().peekable();
739 let mut in_brackets = false;
740 while let Some(c) = chars.next() {
741 if in_brackets {
742 out.push(c);
743 if c == ']' {
744 in_brackets = false;
745 }
746 continue;
747 }
748 match c {
749 '%' => out.push_str(".*"),
750 '_' => out.push('.'),
751 '[' => {
752 in_brackets = true;
753 out.push('[');
754 }
755 '\\' => {
756 if let Some(next) = chars.next() {
759 for e in regex::escape(&next.to_string()).chars() {
760 out.push(e);
761 }
762 }
763 }
764 '.' | '^' | '$' => {
765 out.push('\\');
766 out.push(c);
767 }
768 other => out.push(other),
769 }
770 }
771 out.push_str(")$");
772 out
773}
774
775#[cfg(test)]
776mod regex_tests {
777 use super::compile_pg_regex;
778
779 #[test]
780 fn postgres_regex_flags_control_expansion_quoting_and_newlines() {
781 assert!(compile_pg_regex("a b", "x", false).unwrap().is_match("ab"));
782 assert!(!compile_pg_regex("a b", "x", false).unwrap().is_match("a b"));
783 assert!(compile_pg_regex("a.b", "q", false).unwrap().is_match("a.b"));
784 assert!(compile_pg_regex("a.b", "", false).unwrap().is_match("a\nb"));
785 assert!(!compile_pg_regex("a.b", "n", false)
786 .unwrap()
787 .is_match("a\nb"));
788 assert!(!compile_pg_regex("[^a]", "n", false).unwrap().is_match("\n"));
789 assert!(compile_pg_regex("[^]a]", "n", false).unwrap().is_match("b"));
790 assert!(!compile_pg_regex("[^]a]", "n", false).unwrap().is_match("]"));
791 assert!(!compile_pg_regex("[^]a]", "n", false)
792 .unwrap()
793 .is_match("\n"));
794 assert!(!compile_pg_regex(r"[^\n]", "en", false)
795 .unwrap()
796 .is_match("\n"));
797 for flags in ["m", "n", "p"] {
798 assert!(compile_pg_regex("[^-a]", flags, false)
799 .unwrap()
800 .is_match("1"));
801 assert!(!compile_pg_regex("[^-a]", flags, false)
802 .unwrap()
803 .is_match("\n"));
804 }
805 for flags in ["", "n"] {
806 assert!(compile_pg_regex("[[]", flags, false).unwrap().is_match("["));
807 assert!(compile_pg_regex("[^[]", flags, false)
808 .unwrap()
809 .is_match("1"));
810 assert!(!compile_pg_regex("[^[]", flags, false)
811 .unwrap()
812 .is_match("["));
813 }
814 assert!(compile_pg_regex("[^a]", "s", false).unwrap().is_match("\n"));
815 assert!(compile_pg_regex("[ ]", "x", false).unwrap().is_match(" "));
816 assert!(compile_pg_regex("[[:digit:] ]", "x", false)
817 .unwrap()
818 .is_match(" "));
819 assert!(compile_pg_regex("[[:digit:]#]", "x", false)
820 .unwrap()
821 .is_match("#"));
822 assert!(compile_pg_regex("a # ignored\n b", "x", false)
823 .unwrap()
824 .is_match("ab"));
825 for literal in ['\u{0085}', '\u{00A0}', '\u{2007}', '\u{202F}'] {
826 let pattern = format!("a{literal}b");
827 assert!(!compile_pg_regex(&pattern, "x", false)
828 .unwrap()
829 .is_match("ab"));
830 assert!(compile_pg_regex(&pattern, "x", false)
831 .unwrap()
832 .is_match(&pattern));
833 }
834 assert!(compile_pg_regex("a\u{2003}b", "x", false)
835 .unwrap()
836 .is_match("ab"));
837 for syntax in ["b", "e"] {
838 assert!(compile_pg_regex("a+", syntax, false)
839 .unwrap()
840 .is_match("a+"));
841 assert!(!compile_pg_regex("a+", syntax, false)
842 .unwrap()
843 .is_match("aa"));
844 }
845 assert!(compile_pg_regex(r"a\{1,\}", "b", false)
846 .unwrap()
847 .is_match("aa"));
848 assert!(compile_pg_regex(r"\d", "b", false).unwrap().is_match("d"));
849 assert!(!compile_pg_regex(r"\d", "b", false).unwrap().is_match("1"));
850 for syntax in ["b", "e"] {
851 assert!(compile_pg_regex("a^b", syntax, false)
852 .unwrap()
853 .is_match("a^b"));
854 assert!(compile_pg_regex("a$b", syntax, false)
855 .unwrap()
856 .is_match("a$b"));
857 assert!(compile_pg_regex("^ab$", syntax, false)
858 .unwrap()
859 .is_match("ab"));
860 }
861 }
862
863 #[test]
864 fn postgres_regex_rejects_invalid_options_with_pg_sqlstate() {
865 let error = compile_pg_regex("a", "z", false).unwrap_err();
866 assert_eq!(error.sqlstate(), Some("22023"));
867 let error = compile_pg_regex("a", "g", false).unwrap_err();
868 assert_eq!(error.sqlstate(), Some("22023"));
869 for flags in ["qn", "qp", "qw", "qx"] {
870 let error = compile_pg_regex("a", flags, false).unwrap_err();
871 assert_eq!(error.sqlstate(), Some("2201B"));
872 }
873 assert!(compile_pg_regex("a", "qns", false).unwrap().is_match("a"));
874 }
875}