From e68b8a59caa06482a41dcc3d7f1f99583a1d3830 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Fri, 8 May 2026 18:32:13 -0400 Subject: [PATCH 1/2] parser: validate escape sequences for literals --- .../rewrite_as_dollar_quoted_string.rs | 2 +- crates/squawk_parser/src/lexed_str.rs | 237 +++++++++++++++--- crates/squawk_syntax/src/lib.rs | 1 + ...est__unicode_escape_string_validation.snap | 136 ++++++++++ crates/squawk_syntax/src/test.rs | 22 ++ crates/squawk_syntax/src/validation.rs | 138 ++++++++++ .../validation/unicode_escape_string.sql | 12 + crates/xtask/src/sync_pg.rs | 6 + postgres/kwlist.h | 6 +- postgres/regression_suite/aggregates.sql | 17 ++ postgres/regression_suite/bit.sql | 8 +- postgres/regression_suite/cluster.sql | 2 +- .../regression_suite/collate.icu.utf8.sql | 138 ++++++++++ postgres/regression_suite/fast_default.sql | 16 ++ postgres/regression_suite/graph_table.sql | 10 +- postgres/regression_suite/partition_merge.sql | 44 ++-- postgres/regression_suite/partition_split.sql | 78 +++--- postgres/regression_suite/sqljson.sql | 8 + postgres/regression_suite/strings.sql | 4 +- postgres/regression_suite/triggers.sql | 23 ++ squawk-vscode/src/extension.ts | 2 +- 21 files changed, 801 insertions(+), 109 deletions(-) create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__unicode_escape_string_validation.snap create mode 100644 crates/squawk_syntax/test_data/validation/unicode_escape_string.sql diff --git a/crates/squawk_ide/src/code_actions/rewrite_as_dollar_quoted_string.rs b/crates/squawk_ide/src/code_actions/rewrite_as_dollar_quoted_string.rs index 1b8c88d0b..f91ebee40 100644 --- a/crates/squawk_ide/src/code_actions/rewrite_as_dollar_quoted_string.rs +++ b/crates/squawk_ide/src/code_actions/rewrite_as_dollar_quoted_string.rs @@ -117,7 +117,7 @@ mod test { fn rewrite_prefix_string_not_applicable() { assert!(code_action_not_applicable( rewrite_as_dollar_quoted_string, - "select b'foo$0';" + "select b'010$0';" )); } } diff --git a/crates/squawk_parser/src/lexed_str.rs b/crates/squawk_parser/src/lexed_str.rs index 9b1e25e26..803117ec0 100644 --- a/crates/squawk_parser/src/lexed_str.rs +++ b/crates/squawk_parser/src/lexed_str.rs @@ -170,20 +170,10 @@ impl<'a> Converter<'a> { squawk_lexer::TokenKind::Whitespace => SyntaxKind::WHITESPACE, squawk_lexer::TokenKind::Ident => { - // TODO: check for max identifier length - // - // see: https://www.postgresql.org/docs/16/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS - // The system uses no more than NAMEDATALEN-1 bytes of an - // identifier; longer names can be written in commands, but - // they will be truncated. By default, NAMEDATALEN is 64 so - // the maximum identifier length is 63 bytes. If this limit - // is problematic, it can be raised by changing the - // NAMEDATALEN constant in src/include/pg_config_manual.h. - // see: https://github.com/postgres/postgres/blob/e032e4c7ddd0e1f7865b246ec18944365d4f8614/src/include/pg_config_manual.h#L29 SyntaxKind::from_keyword(token_text).unwrap_or(SyntaxKind::IDENT) } squawk_lexer::TokenKind::Literal { kind, .. } => { - self.extend_literal(token_text.len(), kind); + self.extend_literal(token_text, kind); return; } squawk_lexer::TokenKind::Semi => SyntaxKind::SEMICOLON, @@ -233,13 +223,13 @@ impl<'a> Converter<'a> { self.push(syntax_kind, token_text.len(), err); } - fn extend_literal(&mut self, len: usize, kind: &squawk_lexer::LiteralKind) { - let mut err = ""; + fn extend_literal(&mut self, token_text: &str, kind: &squawk_lexer::LiteralKind) { + let mut err: Option = None; let syntax_kind = match *kind { squawk_lexer::LiteralKind::Int { empty_int, base: _ } => { if empty_int { - err = "Missing digits after the integer base prefix"; + err = Some("Missing digits after the integer base prefix".into()); } SyntaxKind::INT_NUMBER } @@ -248,56 +238,245 @@ impl<'a> Converter<'a> { base: _, } => { if empty_exponent { - err = "Missing digits after the exponent symbol"; + err = Some("Missing digits after the exponent symbol".into()); } SyntaxKind::FLOAT_NUMBER } squawk_lexer::LiteralKind::Str { terminated } => { if !terminated { - err = "Missing trailing `'` symbol to terminate the string literal"; + err = + Some("Missing trailing `'` symbol to terminate the string literal".into()); } - // TODO: rust analzyer checks for un-escaped strings, we should too SyntaxKind::STRING } squawk_lexer::LiteralKind::ByteStr { terminated } => { if !terminated { - err = "Missing trailing `'` symbol to terminate the hex bit string literal"; + err = Some( + "Missing trailing `'` symbol to terminate the hex bit string literal" + .into(), + ); + } else { + let inside = &token_text[2..token_text.len() - 1]; + if let Some(c) = inside.chars().find(|c| !c.is_ascii_hexdigit()) { + err = Some(format!("\"{c}\" is not a valid hexadecimal digit")); + } } - // TODO: rust analzyer checks for un-escaped strings, we should too SyntaxKind::BYTE_STRING } squawk_lexer::LiteralKind::BitStr { terminated } => { if !terminated { - err = "Missing trailing `\'` symbol to terminate the bit string literal"; + err = Some( + "Missing trailing `'` symbol to terminate the bit string literal".into(), + ); + } else { + let inside = &token_text[2..token_text.len() - 1]; + if let Some(c) = inside.chars().find(|&c| c != '0' && c != '1') { + err = Some(format!("\"{c}\" is not a valid binary digit")); + } } - // TODO: rust analzyer checks for un-escaped strings, we should too SyntaxKind::BIT_STRING } squawk_lexer::LiteralKind::DollarQuotedString { terminated } => { if !terminated { // TODO: we could be fancier and say the ending string we're looking for - err = "Unterminated dollar quoted string literal"; + err = Some("Unterminated dollar quoted string literal".into()); } - // TODO: rust analzyer checks for un-escaped strings, we should too SyntaxKind::DOLLAR_QUOTED_STRING } squawk_lexer::LiteralKind::UnicodeEscStr { terminated } => { if !terminated { - err = "Missing trailing `'` symbol to terminate the unicode escape string literal"; + err = Some( + "Missing trailing `'` symbol to terminate the unicode escape string literal" + .into(), + ); } - // TODO: rust analzyer checks for un-escaped strings, we should too + // validated in squawk_syntax SyntaxKind::UNICODE_ESC_STRING } squawk_lexer::LiteralKind::EscStr { terminated } => { if !terminated { - err = "Missing trailing `\'` symbol to terminate the escape string literal"; + err = Some( + "Missing trailing `'` symbol to terminate the escape string literal".into(), + ); + } else { + err = validate_escape_string_unicode_escapes(token_text); } - // TODO: rust analzyer checks for un-escaped strings, we should too SyntaxKind::ESC_STRING } }; - let err = if err.is_empty() { None } else { Some(err) }; - self.push(syntax_kind, len, err); + self.push(syntax_kind, token_text.len(), err.as_deref()); + } +} + +fn validate_escape_string_unicode_escapes(token_text: &str) -> Option { + let mut chars = token_text[2..token_text.len() - 1].chars(); + + while let Some(c) = chars.next() { + if c != '\\' { + continue; + } + + let (required, example) = match chars.next() { + Some('u') => (4, r"\uXXXX"), + Some('U') => (8, r"\UXXXXXXXX"), + _ => continue, + }; + + for _ in 0..required { + if !chars.next().is_some_and(|c| c.is_ascii_hexdigit()) { + return Some(format!( + "Unicode escape requires {required} hex digits: {example}" + )); + } + } + } + + None +} + +#[cfg(test)] +mod tests { + use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle}; + use insta::assert_snapshot; + + use super::LexedStr; + + fn lex(text: &str) -> String { + let lexed = LexedStr::new(text); + let renderer = Renderer::plain().decor_style(DecorStyle::Unicode); + let mut res = String::new(); + + for (token, msg) in lexed.errors() { + let group = Level::ERROR.primary_title(msg).element( + Snippet::source(text) + .fold(true) + .annotation(AnnotationKind::Primary.span(lexed.text_range(token))), + ); + res.push_str(&renderer.render(&[group]).to_string()); + res.push('\n'); + } + + res + } + + #[test] + fn empty_int_error() { + assert_snapshot!(lex("select 0x;"), @" + error: Missing digits after the integer base prefix + ╭▸ + 1 │ select 0x; + ╰╴ ━━ + "); + } + + #[test] + fn empty_exponent_error() { + assert_snapshot!(lex("select 1e;"), @" + error: Missing digits after the exponent symbol + ╭▸ + 1 │ select 1e; + ╰╴ ━━ + "); + } + + #[test] + fn unterminated_string_error() { + assert_snapshot!(lex("select 'hello;"), @" + error: Missing trailing `'` symbol to terminate the string literal + ╭▸ + 1 │ select 'hello; + ╰╴ ━━━━━━━ + "); + } + + #[test] + fn hex_invalid_digit() { + assert_snapshot!(lex("select X'1FZ';"), @r#" + error: "Z" is not a valid hexadecimal digit + ╭▸ + 1 │ select X'1FZ'; + ╰╴ ━━━━━━ + "#); + } + + #[test] + fn unterminated_hex_bit_string_error() { + assert_snapshot!(lex("select X'1F;"), @" + error: Missing trailing `'` symbol to terminate the hex bit string literal + ╭▸ + 1 │ select X'1F; + ╰╴ ━━━━━ + "); + } + + #[test] + fn unterminated_bit_string_error() { + assert_snapshot!(lex("select B'101;"), @" + error: Missing trailing `'` symbol to terminate the bit string literal + ╭▸ + 1 │ select B'101; + ╰╴ ━━━━━━ + "); + } + + #[test] + fn invalid_binary_digit_error() { + assert_snapshot!(lex("select b'0 ';"), @r#" + error: " " is not a valid binary digit + ╭▸ + 1 │ select b'0 '; + ╰╴ ━━━━━ + "#); + } + + #[test] + fn unterminated_dollar_quoted_string_error() { + assert_snapshot!(lex("select $tag$hello;"), @" + error: Unterminated dollar quoted string literal + ╭▸ + 1 │ select $tag$hello; + ╰╴ ━━━━━━━━━━━ + "); + } + + #[test] + fn unterminated_unicode_escape_string_error() { + assert_snapshot!(lex("select U&'hello;"), @" + error: Missing trailing `'` symbol to terminate the unicode escape string literal + ╭▸ + 1 │ select U&'hello; + ╰╴ ━━━━━━━━━ + "); + } + + #[test] + fn unterminated_escape_string_error() { + assert_snapshot!(lex("select E'hello;"), @" + error: Missing trailing `'` symbol to terminate the escape string literal + ╭▸ + 1 │ select E'hello; + ╰╴ ━━━━━━━━ + "); + } + + #[test] + fn invalid_unicode_escape_4_digits_error() { + assert_snapshot!(lex(r"select E'\u00';"), @r" + error: Unicode escape requires 4 hex digits: \uXXXX + ╭▸ + 1 │ select E'\u00'; + ╰╴ ━━━━━━━ + "); + } + + #[test] + fn invalid_unicode_escape_8_digits_error() { + assert_snapshot!(lex(r"select E'\UFFFF';"), @r" + error: Unicode escape requires 8 hex digits: \UXXXXXXXX + ╭▸ + 1 │ select E'\UFFFF'; + ╰╴ ━━━━━━━━━ + "); } } diff --git a/crates/squawk_syntax/src/lib.rs b/crates/squawk_syntax/src/lib.rs index 4863d94f2..81c40960a 100644 --- a/crates/squawk_syntax/src/lib.rs +++ b/crates/squawk_syntax/src/lib.rs @@ -95,6 +95,7 @@ impl Parse { vec![] }; validation::validate(&self.syntax_node(), &mut errors); + errors.sort_by_key(|error| error.range().start()); errors } } diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__unicode_escape_string_validation.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__unicode_escape_string_validation.snap new file mode 100644 index 000000000..8430a1062 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__unicode_escape_string_validation.snap @@ -0,0 +1,136 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_syntax/test_data/validation/unicode_escape_string.sql +--- +SOURCE_FILE@0..241 + COMMENT@0..5 "-- ok" + WHITESPACE@5..6 "\n" + SELECT@6..30 + SELECT_CLAUSE@6..30 + SELECT_KW@6..12 "select" + WHITESPACE@12..13 " " + TARGET_LIST@13..30 + TARGET@13..30 + LITERAL@13..30 + UNICODE_ESC_STRING@13..30 "U&'\\0061\\+000061'" + SEMICOLON@30..31 ";" + WHITESPACE@31..32 "\n" + SELECT@32..45 + SELECT_CLAUSE@32..45 + SELECT_KW@32..38 "select" + WHITESPACE@38..39 " " + TARGET_LIST@39..45 + TARGET@39..45 + LITERAL@39..45 + UNICODE_ESC_STRING@39..45 "U&'\\\\'" + SEMICOLON@45..46 ";" + WHITESPACE@46..47 "\n" + SELECT@47..79 + SELECT_CLAUSE@47..79 + SELECT_KW@47..53 "select" + WHITESPACE@53..54 " " + TARGET_LIST@54..79 + TARGET@54..79 + LITERAL@54..79 + UNICODE_ESC_STRING@54..67 "U&'ok: !0061'" + WHITESPACE@67..68 " " + UESCAPE_KW@68..75 "UESCAPE" + WHITESPACE@75..76 " " + STRING@76..79 "'!'" + SEMICOLON@79..80 ";" + WHITESPACE@80..81 "\n" + SELECT@81..106 + SELECT_CLAUSE@81..106 + SELECT_KW@81..87 "select" + WHITESPACE@87..88 " " + TARGET_LIST@88..106 + TARGET@88..106 + LITERAL@88..106 + UNICODE_ESC_STRING@88..94 "U&' \\'" + WHITESPACE@94..95 " " + UESCAPE_KW@95..102 "UESCAPE" + WHITESPACE@102..103 " " + STRING@103..106 "'!'" + SEMICOLON@106..107 ";" + WHITESPACE@107..109 "\n\n" + COMMENT@109..118 "-- errors" + WHITESPACE@118..119 "\n" + SELECT@119..134 + SELECT_CLAUSE@119..134 + SELECT_KW@119..125 "select" + WHITESPACE@125..126 " " + TARGET_LIST@126..134 + TARGET@126..134 + LITERAL@126..134 + UNICODE_ESC_STRING@126..134 "U&'\\006'" + SEMICOLON@134..135 ";" + WHITESPACE@135..136 "\n" + SELECT@136..153 + SELECT_CLAUSE@136..153 + SELECT_KW@136..142 "select" + WHITESPACE@142..143 " " + TARGET_LIST@143..153 + TARGET@143..153 + LITERAL@143..153 + UNICODE_ESC_STRING@143..153 "U&'\\+0061'" + SEMICOLON@153..154 ";" + WHITESPACE@154..155 "\n" + SELECT@155..188 + SELECT_CLAUSE@155..188 + SELECT_KW@155..161 "select" + WHITESPACE@161..162 " " + TARGET_LIST@162..188 + TARGET@162..188 + LITERAL@162..188 + UNICODE_ESC_STRING@162..176 "U&'wrong: \\06'" + WHITESPACE@176..177 " " + UESCAPE_KW@177..184 "UESCAPE" + WHITESPACE@184..185 " " + STRING@185..188 "'\\'" + SEMICOLON@188..189 ";" + WHITESPACE@189..190 "\n" + SELECT@190..224 + SELECT_CLAUSE@190..224 + SELECT_KW@190..196 "select" + WHITESPACE@196..197 " " + TARGET_LIST@197..224 + TARGET@197..224 + LITERAL@197..224 + UNICODE_ESC_STRING@197..212 "U&'wrong: !061'" + WHITESPACE@212..213 " " + UESCAPE_KW@213..220 "UESCAPE" + WHITESPACE@220..221 " " + STRING@221..224 "'!'" + SEMICOLON@224..225 ";" + WHITESPACE@225..226 "\n" + SELECT@226..239 + SELECT_CLAUSE@226..239 + SELECT_KW@226..232 "select" + WHITESPACE@232..233 " " + TARGET_LIST@233..239 + TARGET@233..239 + LITERAL@233..239 + UNICODE_ESC_STRING@233..239 "U&' \\'" + SEMICOLON@239..240 ";" + WHITESPACE@240..241 "\n" + +error[syntax-error]: Unicode escape requires 4 hex digits: \XXXX + ╭▸ +8 │ select U&'\006'; + ╰╴ ━━━━━━━━ +error[syntax-error]: Unicode escape requires 6 hex digits: \+XXXXXX + ╭▸ +9 │ select U&'\+0061'; + ╰╴ ━━━━━━━━━━ +error[syntax-error]: Unicode escape requires 4 hex digits: \XXXX + ╭▸ +10 │ select U&'wrong: \06' UESCAPE '\'; + ╰╴ ━━━━━━━━━━━━━━ +error[syntax-error]: Unicode escape requires 4 hex digits: !XXXX + ╭▸ +11 │ select U&'wrong: !061' UESCAPE '!'; + ╰╴ ━━━━━━━━━━━━━━━ +error[syntax-error]: Invalid Unicode escape sequence + ╭▸ +12 │ select U&' \'; + ╰╴ ━━━━━━ diff --git a/crates/squawk_syntax/src/test.rs b/crates/squawk_syntax/src/test.rs index aaded5b41..3d600ce67 100644 --- a/crates/squawk_syntax/src/test.rs +++ b/crates/squawk_syntax/src/test.rs @@ -140,3 +140,25 @@ fn syntaxtest(fixture: Fixture<&str>) { ); } } + +#[test] +fn parse_errors_are_sorted_by_position() { + let sql = "from t select 1 + ;"; + let parse = SourceFile::parse(sql); + let rendered = parse + .errors() + .iter() + .map(|syntax_error| { + let range = syntax_error.range(); + let start: u32 = range.start().into(); + let end: u32 = range.end().into(); + format!("{start}..{end}: {}", syntax_error.message()) + }) + .collect::>() + .join("\n"); + + assert_snapshot!(rendered, @r" + 0..6: Leading from clauses are not supported in Postgres + 17..17: expected an expression, found SEMICOLON + "); +} diff --git a/crates/squawk_syntax/src/validation.rs b/crates/squawk_syntax/src/validation.rs index 5fddc0c19..450b5772d 100644 --- a/crates/squawk_syntax/src/validation.rs +++ b/crates/squawk_syntax/src/validation.rs @@ -4,6 +4,9 @@ //! //! A failed validation emits a diagnostic. +use std::fmt; +use std::ops::RangeInclusive; + use crate::ast::AstNode; use crate::{SyntaxNode, ast, match_ast, syntax_error::SyntaxError}; use rowan::{TextRange, TextSize}; @@ -163,6 +166,141 @@ fn validate_literal(lit: ast::Literal, acc: &mut Vec) { } } } + + if let Some(err) = validate_unicode_esc_string(&lit) { + acc.push(err); + } +} + +fn validate_unicode_esc_string(lit: &ast::Literal) -> Option { + let mut unicode_esc = None; + let mut seen_uescape = false; + let mut escape_char = '\\'; + for e in lit.syntax().children_with_tokens() { + let Some(token) = e.into_token() else { + continue; + }; + match token.kind() { + UNICODE_ESC_STRING => unicode_esc = Some(token), + UESCAPE_KW => seen_uescape = true, + STRING if seen_uescape => { + let text = token.text(); + let inner = text + .strip_prefix('\'') + .and_then(|s| s.strip_suffix('\'')) + .unwrap_or(""); + let mut chars = inner.chars(); + if let (Some(c), None) = (chars.next(), chars.next()) { + escape_char = c; + } + break; + } + _ => (), + } + } + let token = unicode_esc?; + let text = token.text(); + let inside = text + .strip_prefix("U&'") + .or_else(|| text.strip_prefix("u&'")) + .and_then(|s| s.strip_suffix('\''))?; + let err = check_unicode_esc_str(inside, escape_char)?; + Some(SyntaxError::new(err.to_string(), token.text_range())) +} + +enum UnicodeEscapeKind { + Short, + Extended, +} + +impl UnicodeEscapeKind { + fn count(&self) -> u32 { + match self { + UnicodeEscapeKind::Short => 4, + UnicodeEscapeKind::Extended => 6, + } + } +} + +enum UnicodeEscError { + InvalidEscape, + InvalidSurrogatePair, + OutOfRange, + RequiresHexDigits { + kind: UnicodeEscapeKind, + escape_char: char, + }, +} + +impl fmt::Display for UnicodeEscError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidEscape => f.write_str("Invalid Unicode escape sequence"), + Self::InvalidSurrogatePair => f.write_str("Invalid Unicode surrogate pair"), + Self::OutOfRange => f.write_str("Unicode escape value out of range"), + Self::RequiresHexDigits { kind, escape_char } => { + let required = kind.count(); + let plus = match kind { + UnicodeEscapeKind::Short => "", + UnicodeEscapeKind::Extended => "+", + }; + let xs = "X".repeat(required as usize); + write!( + f, + "Unicode escape requires {required} hex digits: {escape_char}{plus}{xs}" + ) + } + } + } +} + +fn check_unicode_esc_str(text: &str, escape_char: char) -> Option { + const HIGH_SURROGATE: RangeInclusive = 0xD800..=0xDBFF; + const LOW_SURROGATE: RangeInclusive = 0xDC00..=0xDFFF; + const MAX_CODEPOINT: u32 = 0x10FFFF; + + let mut chars = text.chars().peekable(); + let mut high_surrogate: Option = None; + + while let Some(c) = chars.next() { + if c != escape_char { + continue; + } + let kind = match chars.peek() { + Some(&c) if c == escape_char => { + chars.next(); + high_surrogate = None; + continue; + } + Some('+') => { + chars.next(); + UnicodeEscapeKind::Extended + } + Some(c) if c.is_ascii_hexdigit() => UnicodeEscapeKind::Short, + _ => return Some(UnicodeEscError::InvalidEscape), + }; + let mut codepoint: u32 = 0; + for _ in 0..kind.count() { + let radix = 16; + let Some(d) = chars.peek().and_then(|c| c.to_digit(radix)) else { + return Some(UnicodeEscError::RequiresHexDigits { kind, escape_char }); + }; + chars.next(); + codepoint = codepoint * radix + d; + } + if high_surrogate.take().is_some() { + if !LOW_SURROGATE.contains(&codepoint) { + return Some(UnicodeEscError::InvalidSurrogatePair); + } + } else if codepoint > MAX_CODEPOINT { + return Some(UnicodeEscError::OutOfRange); + } else if HIGH_SURROGATE.contains(&codepoint) { + high_surrogate = Some(codepoint); + } else if LOW_SURROGATE.contains(&codepoint) { + return Some(UnicodeEscError::InvalidSurrogatePair); + } + } + high_surrogate.map(|_| UnicodeEscError::InvalidSurrogatePair) } fn validate_join_expr(join_expr: ast::JoinExpr, acc: &mut Vec) { diff --git a/crates/squawk_syntax/test_data/validation/unicode_escape_string.sql b/crates/squawk_syntax/test_data/validation/unicode_escape_string.sql new file mode 100644 index 000000000..5e05a65d9 --- /dev/null +++ b/crates/squawk_syntax/test_data/validation/unicode_escape_string.sql @@ -0,0 +1,12 @@ +-- ok +select U&'\0061\+000061'; +select U&'\\'; +select U&'ok: !0061' UESCAPE '!'; +select U&' \' UESCAPE '!'; + +-- errors +select U&'\006'; +select U&'\+0061'; +select U&'wrong: \06' UESCAPE '\'; +select U&'wrong: !061' UESCAPE '!'; +select U&' \'; diff --git a/crates/xtask/src/sync_pg.rs b/crates/xtask/src/sync_pg.rs index 3657b2db3..d32ea3070 100644 --- a/crates/xtask/src/sync_pg.rs +++ b/crates/xtask/src/sync_pg.rs @@ -92,6 +92,12 @@ const IGNORED_LINES: &[&str] = &[ "CREATE VIEW foo AS SELECT 1 INTO int4_tbl;", "INSERT INTO int4_tbl SELECT 1 INTO f;", "REPACK (CONCURRENTLY) :toast_rel;", + r#"SELECT E'wrong: \u061';"#, + r#"SELECT E'wrong: \U0061';"#, + r#"SELECT x'0 ';"#, + r#"SELECT x' 0';"#, + r#"SELECT b' 0';"#, + r#"SELECT b'0 ';"#, ]; const VARIABLE_REPLACEMENTS: &[(&str, &str)] = &[ diff --git a/postgres/kwlist.h b/postgres/kwlist.h index 392276257..170e24ded 100644 --- a/postgres/kwlist.h +++ b/postgres/kwlist.h @@ -1,7 +1,7 @@ // synced from: -// commit: c06d1a4ba6b26eef27b04074683cccade6c277ee -// committed at: 2026-05-03T20:23:50+03:00 -// file: https://github.com/postgres/postgres/blob/c06d1a4ba6b26eef27b04074683cccade6c277ee/src/include/parser/kwlist.h +// commit: 901ed9b352b41f034e17bc540725082a488fce31 +// committed at: 2026-05-08T16:44:25+07:00 +// file: https://github.com/postgres/postgres/blob/901ed9b352b41f034e17bc540725082a488fce31/src/include/parser/kwlist.h // // update via: // cargo xtask sync-pg diff --git a/postgres/regression_suite/aggregates.sql b/postgres/regression_suite/aggregates.sql index 048029118..968b3ea9b 100644 --- a/postgres/regression_suite/aggregates.sql +++ b/postgres/regression_suite/aggregates.sql @@ -577,6 +577,23 @@ alter table t2 alter column z drop not null; create unique index t2_z_uidx on t2(z) nulls not distinct; explain (costs off) select y,z from t2 group by y,z; +-- A unique index proves uniqueness only under its own opfamily. When the +-- GROUP BY's eqop comes from a different opfamily with looser equality, +-- rows the index regards as distinct can collapse into one GROUP BY group, +-- so the index is not usable for removing redundant columns. +create type t_rec as (x numeric); +create temp table t_opf (a t_rec not null, b text); +create unique index on t_opf (a record_image_ops); +-- (1.0) and (1.00) are bytewise distinct but logically equal as records; +-- the index admits both, but GROUP BY a (default record_ops) would merge +-- them, so b must be retained as a grouping key. +insert into t_opf values (row(1.0)::t_rec, 'X'), (row(1.00)::t_rec, 'Y'); +explain (costs off) +select a, b from t_opf group by a, b order by b; +select a, b from t_opf group by a, b order by b; +drop table t_opf; +drop type t_rec; + drop table t1 cascade; drop table t2; drop table t3; diff --git a/postgres/regression_suite/bit.sql b/postgres/regression_suite/bit.sql index 9a719cd23..116c2e044 100644 --- a/postgres/regression_suite/bit.sql +++ b/postgres/regression_suite/bit.sql @@ -30,10 +30,10 @@ INSERT INTO VARBIT_TABLE VALUES (B'101011111010'); -- too long SELECT * FROM VARBIT_TABLE; -- Literals with syntax errors -SELECT b' 0'; -SELECT b'0 '; -SELECT x' 0'; -SELECT x'0 '; +-- SELECT b' 0'; +-- SELECT b'0 '; +-- SELECT x' 0'; +-- SELECT x'0 '; -- Concatenation SELECT v, b, (v || b) AS concat diff --git a/postgres/regression_suite/cluster.sql b/postgres/regression_suite/cluster.sql index 5dac9ec4e..ab073600e 100644 --- a/postgres/regression_suite/cluster.sql +++ b/postgres/regression_suite/cluster.sql @@ -409,7 +409,7 @@ CREATE TABLE repack_conc_toast (t text); SELECT reltoastrelid::regclass AS toast_rel FROM pg_class WHERE oid = 'repack_conc_toast'::regclass /* \gset */; -- \set VERBOSITY sqlstate --- REPACK (CONCURRENTLY) 'toast_rel'; +-- REPACK (CONCURRENTLY) :toast_rel; -- \set VERBOSITY default DROP TABLE repack_conc_toast; diff --git a/postgres/regression_suite/collate.icu.utf8.sql b/postgres/regression_suite/collate.icu.utf8.sql index 281ca00b8..f1ea99447 100644 --- a/postgres/regression_suite/collate.icu.utf8.sql +++ b/postgres/regression_suite/collate.icu.utf8.sql @@ -612,6 +612,109 @@ CREATE UNIQUE INDEX ON test3cs (x); -- ok SELECT string_to_array('ABC,DEF,GHI' COLLATE case_sensitive, ',', 'abc'); SELECT string_to_array('ABCDEFGHI' COLLATE case_sensitive, NULL, 'b'); +-- +-- A unique index under one collation does not prove uniqueness under +-- another, so the planner must not use such a proof for any optimization. +-- + +-- Ensure that we do not use inner-unique join execution +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM test1cs t1, test3cs t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +SELECT * FROM test1cs t1, test3cs t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +-- Ensure that left-join is not removed +EXPLAIN (COSTS OFF) +SELECT t1.* FROM test3cs t1 + LEFT JOIN test3cs t2 ON t1.x = t2.x COLLATE case_insensitive +ORDER BY 1; + +SELECT t1.* FROM test3cs t1 + LEFT JOIN test3cs t2 ON t1.x = t2.x COLLATE case_insensitive +ORDER BY 1; + +-- Ensure that self-join is not removed +EXPLAIN (COSTS OFF) +SELECT * FROM test3cs t1, test3cs t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +SELECT * FROM test3cs t1, test3cs t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +-- Ensure that semijoin is not reduced to innerjoin +EXPLAIN (COSTS OFF) +SELECT * FROM test3cs t1 + WHERE EXISTS (SELECT 1 FROM test3cs t2 WHERE t1.x = t2.x COLLATE case_insensitive) +ORDER BY 1; + +SELECT * FROM test3cs t1 + WHERE EXISTS (SELECT 1 FROM test3cs t2 WHERE t1.x = t2.x COLLATE case_insensitive) +ORDER BY 1; + +-- +-- A DISTINCT / GROUP BY / set-op on a subquery does not prove uniqueness +-- under a different collation, so the planner must not use such a proof for +-- any optimization. +-- + +-- Ensure that we do not use inner-unique join execution +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM test1cs t1, (SELECT DISTINCT x FROM test3cs) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +SELECT * FROM test1cs t1, (SELECT DISTINCT x FROM test3cs) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +-- Same with GROUP BY +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM test1cs t1, (SELECT x FROM test3cs GROUP BY x) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +SELECT * FROM test1cs t1, (SELECT x FROM test3cs GROUP BY x) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +-- Same with set-op +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM test1cs t1, (SELECT x FROM test3cs UNION SELECT x FROM test3cs) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +SELECT * FROM test1cs t1, (SELECT x FROM test3cs UNION SELECT x FROM test3cs) t2 +WHERE t1.x = t2.x COLLATE case_insensitive +ORDER BY 1, 2; + +-- Ensure that left-join is not removed +EXPLAIN (COSTS OFF) +SELECT t1.* FROM test3cs t1 + LEFT JOIN (SELECT DISTINCT x FROM test3cs) t2 ON t1.x = t2.x COLLATE case_insensitive +ORDER BY 1; + +SELECT t1.* FROM test3cs t1 + LEFT JOIN (SELECT DISTINCT x FROM test3cs) t2 ON t1.x = t2.x COLLATE case_insensitive +ORDER BY 1; + +-- Ensure that semijoin is not reduced to innerjoin +EXPLAIN (COSTS OFF) +SELECT * FROM test3cs t1 + WHERE EXISTS (SELECT 1 FROM (SELECT DISTINCT x FROM test3cs) t2 + WHERE t1.x = t2.x COLLATE case_insensitive) +ORDER BY 1; + +SELECT * FROM test3cs t1 + WHERE EXISTS (SELECT 1 FROM (SELECT DISTINCT x FROM test3cs) t2 + WHERE t1.x = t2.x COLLATE case_insensitive) +ORDER BY 1; + CREATE TABLE test1ci (x text COLLATE case_insensitive); CREATE TABLE test2ci (x text COLLATE case_insensitive); CREATE TABLE test3ci (x text COLLATE case_insensitive); @@ -694,6 +797,21 @@ EXPLAIN (COSTS OFF) SELECT x, count(*) FROM test3ci GROUP BY x HAVING ROW(x, 1) < ROW('ABC' COLLATE case_sensitive, 1) ORDER BY 1; SELECT x, count(*) FROM test3ci GROUP BY x HAVING ROW(x, 1) < ROW('ABC' COLLATE case_sensitive, 1) ORDER BY 1; +-- Negative: simple-CASE form with conflicting WHEN comparison collation +EXPLAIN (COSTS OFF) +SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE x WHEN 'abc' COLLATE case_sensitive THEN true ELSE false END); +SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE x WHEN 'abc' COLLATE case_sensitive THEN true ELSE false END); + +-- Positive: simple-CASE form with matching collation, safe to push +EXPLAIN (COSTS OFF) +SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE x WHEN 'abc' COLLATE case_insensitive THEN true ELSE false END); +SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE x WHEN 'abc' COLLATE case_insensitive THEN true ELSE false END); + +-- Negative: nested CASE with collation conflict +EXPLAIN (COSTS OFF) +SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE WHEN (CASE x WHEN 'abc' COLLATE case_sensitive THEN 1 ELSE 0 END) = 1 THEN true ELSE false END); +SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE WHEN (CASE x WHEN 'abc' COLLATE case_sensitive THEN 1 ELSE 0 END) = 1 THEN true ELSE false END); + -- Positive: conflicting collation but no grouping expression reference EXPLAIN (COSTS OFF) SELECT x, count(*) FROM test3ci GROUP BY x HAVING current_setting('server_version') = 'abc' COLLATE case_sensitive; @@ -1101,6 +1219,26 @@ GROUP BY t1.id, t1.val; DROP TABLE eager_agg_t1; DROP TABLE eager_agg_t2; +-- +-- A unique index can prove functional dependency for GROUP BY column +-- removal only if its per-column collation agrees on equality with +-- the GROUP BY column's collation. An index built under a different +-- (deterministic) collation would otherwise let remove_useless_groupby_columns +-- drop other columns whose values still differ within a nondeterministic +-- group. +-- +CREATE TABLE groupby_collation_t (a text COLLATE case_insensitive NOT NULL, b text); +INSERT INTO groupby_collation_t VALUES ('foo', 'X'), ('FOO', 'Y'); +CREATE UNIQUE INDEX ON groupby_collation_t (a COLLATE "C"); + +-- Column b must NOT be dropped: under case_insensitive on a, 'foo' and +-- 'FOO' would merge, but they have distinct b values. +EXPLAIN (COSTS OFF) +SELECT a, b FROM groupby_collation_t GROUP BY a, b ORDER BY a, b; +SELECT a, b FROM groupby_collation_t GROUP BY a, b ORDER BY a, b; + +DROP TABLE groupby_collation_t; + -- virtual generated columns CREATE TABLE t5 ( a int, diff --git a/postgres/regression_suite/fast_default.sql b/postgres/regression_suite/fast_default.sql index 8b31d317d..e5d9a3d2f 100644 --- a/postgres/regression_suite/fast_default.sql +++ b/postgres/regression_suite/fast_default.sql @@ -653,6 +653,22 @@ SELECT count(*) WHERE attrelid = 'ft1'::regclass AND (attmissingval IS NOT NULL OR atthasmissing); +-- Verify that table-rewriting maintenance commands preserve attmissingval +-- columns. +CREATE TABLE t (id int PRIMARY KEY); +INSERT INTO t SELECT generate_series(1, 3); +ALTER TABLE t ADD COLUMN a int DEFAULT 42; +ALTER TABLE t ADD COLUMN b int NOT NULL DEFAULT 7 CHECK (b > 0); +VACUUM FULL t; +SELECT * FROM t ORDER BY id; +ALTER TABLE t ADD COLUMN c text DEFAULT 'hello'; +CLUSTER t USING t_pkey; +SELECT * FROM t ORDER BY id; +ALTER TABLE t ADD COLUMN d int DEFAULT 99; +REPACK t; +SELECT * FROM t ORDER BY id; +DROP TABLE t; + -- cleanup DROP FOREIGN TABLE ft1; DROP SERVER s0; diff --git a/postgres/regression_suite/graph_table.sql b/postgres/regression_suite/graph_table.sql index f272b5915..6da35a44d 100644 --- a/postgres/regression_suite/graph_table.sql +++ b/postgres/regression_suite/graph_table.sql @@ -215,7 +215,7 @@ CREATE PROPERTY GRAPH g1 VERTEX TABLES ( v1 LABEL vl1 PROPERTIES (vname, vprop1) - LABEL l1 PROPERTIES (vname AS elname), -- label shared by vertexes as well as edges + LABEL l1 PROPERTIES (vname AS elname), -- label shared by vertices as well as edges v2 KEY (id1, id2) LABEL vl2 PROPERTIES (vname, vprop2, 'vl2_prop'::varchar(10) AS lprop1) LABEL vl3 PROPERTIES (vname, vprop1, 'vl2_prop'::varchar(10) AS lprop1) @@ -519,7 +519,7 @@ CREATE PROPERTY GRAPH g4 DESTINATION KEY (dest) REFERENCES ptnv(id) ); SELECT * FROM GRAPH_TABLE (g4 MATCH (s IS ptnv)-[e IS ptne]->(d IS ptnv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; --- edges from the same vertex in both directions connecting to other vertexes in the same table +-- edges from the same vertex in both directions connecting to other vertices in the same table SELECT * FROM GRAPH_TABLE (g4 MATCH (s)-[e]-(d) WHERE s.id = 3 COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; SELECT * FROM GRAPH_TABLE (g4 MATCH (s WHERE s.id = 3)-[e]-(d) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; @@ -590,4 +590,10 @@ SELECT * FROM customers co WHERE co.customer_id = (SELECT customer_id FROM GRAPH SELECT sname, dname FROM GRAPH_TABLE (g1 MATCH (src)->(dest) WHERE src.vprop1 > (SELECT max(v1.vprop1) FROM v1) COLUMNS(src.vname AS sname, dest.vname AS dname)); SELECT sname, dname FROM GRAPH_TABLE (g1 MATCH (src)->(dest) WHERE out_degree(src.vname) > (SELECT max(out_degree(nname)) FROM GRAPH_TABLE (g1 MATCH (node) COLUMNS (node.vname AS nname))) COLUMNS(src.vname AS sname, dest.vname AS dname)); +-- GRAPH_TABLE subquery in HAVING clause (tests expression mutator) +SELECT src.vname, count(*) FROM v1 AS src + GROUP BY src.vname + HAVING count(*) >= (SELECT count(*) FROM GRAPH_TABLE (g1 MATCH (a IS vl1 | vl2) COLUMNS (a.vname AS n)) WHERE n = src.vname) + ORDER BY vname; + -- leave the objects behind for pg_upgrade/pg_dump tests diff --git a/postgres/regression_suite/partition_merge.sql b/postgres/regression_suite/partition_merge.sql index e0e26f18e..bf1fe3570 100644 --- a/postgres/regression_suite/partition_merge.sql +++ b/postgres/regression_suite/partition_merge.sql @@ -27,21 +27,19 @@ ALTER TABLE sales_range ATTACH PARTITION sales_apr2022 FOR VALUES FROM ('2022-04 CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; --- ERROR: partition with name "sales_feb2022" is already used +-- ERROR ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_feb2022) INTO sales_feb_mar_apr2022; --- ERROR: "sales_apr2022" is not a table +-- ERROR ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO sales_feb_mar_apr2022; --- ERROR: can not merge partition "sales_mar2022" together with partition "sales_jan2022" --- DETAIL: lower bound of partition "sales_mar2022" is not equal to the upper bound of partition "sales_jan2022" +-- ERROR -- (space between sections sales_jan2022 and sales_mar2022) ALTER TABLE sales_range MERGE PARTITIONS (sales_jan2022, sales_mar2022) INTO sales_jan_mar2022; --- ERROR: can not merge partition "sales_jan2022" together with partition "sales_dec2021" --- DETAIL: lower bound of partition "sales_jan2022" is not equal to the upper bound of partition "sales_dec2021" +-- ERROR -- (space between sections sales_dec2021 and sales_jan2022) ALTER TABLE sales_range MERGE PARTITIONS (sales_dec2021, sales_jan2022, sales_feb2022) INTO sales_dec_jan_feb2022; --- ERROR: partition with name "sales_feb2022" is already used +-- ERROR ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, partitions_merge_schema.sales_feb2022) INTO sales_feb_mar_apr2022; ---ERROR, sales_apr_2 already exists +-- ERROR ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_jan2022) INTO sales_apr_2; CREATE VIEW jan2022v as SELECT * FROM sales_jan2022; @@ -357,11 +355,11 @@ CREATE TABLE sales_others2 PARTITION OF sales_list2 DEFAULT; CREATE TABLE sales_external (LIKE sales_list); CREATE TABLE sales_external2 (vch VARCHAR(5)); --- ERROR: "sales_external" is not a partition of partitioned table "sales_list" +-- ERROR ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external) INTO sales_all; --- ERROR: "sales_external2" is not a partition of partitioned table "sales_list" +-- ERROR ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external2) INTO sales_all; --- ERROR: relation "sales_nord2" is not a partition of relation "sales_list" +-- ERROR ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_nord2, sales_east) INTO sales_all; DROP TABLE sales_external2; @@ -438,9 +436,9 @@ CREATE TABLE t2 (i int, t text) PARTITION BY RANGE (t); CREATE TABLE t2pa PARTITION OF t2 FOR VALUES FROM ('A') TO ('C'); CREATE TABLE t3 (i int, t text); --- ERROR: relation "t1p1" is not a partition of relation "t2" +-- ERROR ALTER TABLE t2 MERGE PARTITIONS (t1p1, t2pa) INTO t2p; --- ERROR: "t3" is not a partition of partitioned table "t2" +-- ERROR ALTER TABLE t2 MERGE PARTITIONS (t2pa, t3) INTO t2p; DROP TABLE t3; @@ -481,7 +479,7 @@ ALTER TABLE t MERGE PARTITIONS (tp_0_2, tp_2_3) INTO pg_temp.tp_0_3; -- Partition should be temporary. EXECUTE get_partition_info('{t}'); --- ERROR: cannot create a permanent relation as partition of temporary relation "t" +-- ERROR ALTER TABLE t MERGE PARTITIONS (tp_0_3, tp_3_4) INTO tp_0_4; ROLLBACK; @@ -567,19 +565,19 @@ CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); SET SESSION AUTHORIZATION regress_partition_merge_bob; --- ERROR: must be owner of table t +-- ERROR ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; RESET SESSION AUTHORIZATION; ALTER TABLE t OWNER TO regress_partition_merge_bob; SET SESSION AUTHORIZATION regress_partition_merge_bob; --- ERROR: must be owner of table tp_0_1 +-- ERROR ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; RESET SESSION AUTHORIZATION; ALTER TABLE tp_0_1 OWNER TO regress_partition_merge_bob; SET SESSION AUTHORIZATION regress_partition_merge_bob; --- ERROR: must be owner of table tp_1_2 +-- ERROR ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; RESET SESSION AUTHORIZATION; @@ -607,7 +605,7 @@ ALTER TABLE t ATTACH PARTITION tp_1_2 FOR VALUES FROM (1) TO (2); -- Owner is 'regress_partition_merge_bob': -- \dt tp_1_2 --- ERROR: partitions being merged have different owners +-- ERROR ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; DROP TABLE t; @@ -622,10 +620,10 @@ CREATE TABLE t (i int) PARTITION BY HASH(i); CREATE TABLE tp1 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 0); CREATE TABLE tp2 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 1); --- ERROR: partition of hash-partitioned table cannot be merged +-- ERROR ALTER TABLE t MERGE PARTITIONS (tp1, tp2) INTO tp3; --- ERROR: list of partitions to be merged should include at least two partitions +-- ERROR ALTER TABLE t MERGE PARTITIONS (tp1) INTO tp3; DROP TABLE t; @@ -712,7 +710,7 @@ ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; -- Should be NOT VALID FOREIGN KEY -- \d tp_0_2 --- ERROR: insert or update on table "t_fk" violates foreign key constraint "t_fk_i_fkey" +-- ERROR ALTER TABLE t_fk VALIDATE CONSTRAINT t_fk_i_fkey; DROP TABLE t_fk; @@ -731,7 +729,7 @@ ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; -- Should be NOT ENFORCED FOREIGN KEY -- \d tp_0_2 --- ERROR: insert or update on table "t_fk" violates foreign key constraint "t_fk_i_fkey" +-- ERROR ALTER TABLE t_fk ALTER CONSTRAINT t_fk_i_fkey ENFORCED; DROP TABLE t_fk; @@ -774,7 +772,7 @@ INSERT INTO t VALUES (5), (15); ALTER TABLE t MERGE PARTITIONS (tp_1, tp_2) INTO tp_12; INSERT INTO t VALUES (16); --- ERROR: new row for relation "tp_12" violates check constraint "t_i_check" +-- ERROR INSERT INTO t VALUES (0); -- Should be 3 rows: (5), (15), (16): SELECT i FROM t ORDER BY i; diff --git a/postgres/regression_suite/partition_split.sql b/postgres/regression_suite/partition_split.sql index e3e99d213..ef98e09d5 100644 --- a/postgres/regression_suite/partition_split.sql +++ b/postgres/regression_suite/partition_split.sql @@ -19,75 +19,72 @@ CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01 CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01'); CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; --- ERROR: relation "sales_xxx" does not exist +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_xxx INTO (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); --- ERROR: relation "sales_jan2022" already exists +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_jan2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); --- ERROR: invalid bound specification for a range partition +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_jan2022 FOR VALUES IN ('2022-05-01', '2022-06-01'), PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); --- ERROR: empty range bound specified for partition "sales_mar2022" +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-02-01'), PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); ---ERROR: list of split partitions should contain at least two items +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-10-01')); --- ERROR: lower bound of partition "sales_feb2022" is not equal to lower bound of split partition "sales_feb_mar_apr2022" --- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_feb2022 FOR VALUES FROM ('2022-01-01') TO ('2022-03-01'), PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); --- ERROR: partition with name "sales_feb_mar_apr2022" is already used +-- ERROR -- (We can create partition with the same name as split partition, but can't create two partitions with the same name) ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_feb_mar_apr2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), PARTITION sales_feb_mar_apr2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); --- ERROR: partition with name "sales_feb2022" is already used +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), PARTITION sales_feb2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); --- ERROR: partition with name "sales_feb2022" is already used +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), PARTITION partition_split_schema.sales_feb2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); --- ERROR: ALTER action SPLIT PARTITION cannot be performed on relation "sales_feb_mar_apr2022" --- DETAIL: This operation is not supported for tables. +-- ERROR ALTER TABLE sales_feb_mar_apr2022 SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_jan2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), PARTITION sales_feb2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); --- ERROR: upper bound of partition "sales_apr2022" is not equal to upper bound of split partition "sales_feb_mar_apr2022" --- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-06-01')); --- ERROR: can not split to partition "sales_mar2022" together with partition "sales_feb2022" +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), PARTITION sales_mar2022 FOR VALUES FROM ('2022-02-01') TO ('2022-04-01'), @@ -96,8 +93,7 @@ ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO -- Tests for spaces between partitions, them should be executed without DEFAULT partition ALTER TABLE sales_range DETACH PARTITION sales_others; --- ERROR: lower bound of partition "sales_feb2022" is not equal to lower bound of split partition "sales_feb_mar_apr2022" --- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-02') TO ('2022-03-01'), PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), @@ -121,8 +117,7 @@ CREATE TABLE sales_range (sales_date date) PARTITION BY RANGE (sales_date); CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01'); --- ERROR: upper bound of partition "sales_apr2022" is not equal to upper bound of split partition "sales_feb_mar_apr2022" --- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), @@ -299,7 +294,7 @@ CREATE TABLE sales_range (salesperson_id INT, sales_date date) PARTITION BY RANG CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; -- sales_error intersects with sales_dec2021 (lower bound) --- ERROR: can not split to partition "sales_error" together with partition "sales_dec2021" +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_others INTO (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), PARTITION sales_error FOR VALUES FROM ('2021-12-30') TO ('2022-02-01'), @@ -307,7 +302,7 @@ ALTER TABLE sales_range SPLIT PARTITION sales_others INTO PARTITION sales_others DEFAULT); -- sales_error intersects with sales_feb2022 (upper bound) --- ERROR: can not split to partition "sales_feb2022" together with partition "sales_error" +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_others INTO (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), PARTITION sales_error FOR VALUES FROM ('2022-01-01') TO ('2022-02-02'), @@ -315,7 +310,7 @@ ALTER TABLE sales_range SPLIT PARTITION sales_others INTO PARTITION sales_others DEFAULT); -- sales_error intersects with sales_dec2021 (inside bound) --- ERROR: can not split to partition "sales_error" together with partition "sales_dec2021" +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_others INTO (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), PARTITION sales_error FOR VALUES FROM ('2021-12-10') TO ('2021-12-20'), @@ -323,15 +318,14 @@ ALTER TABLE sales_range SPLIT PARTITION sales_others INTO PARTITION sales_others DEFAULT); -- sales_error intersects with sales_dec2021 (exactly the same bounds) --- ERROR: can not split to partition "sales_error" together with partition "sales_dec2021" +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_others INTO (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), PARTITION sales_error FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), PARTITION sales_others DEFAULT); --- ERROR: can not split DEFAULT partition "sales_others" --- HINT: To split DEFAULT partition one of the new partition must be DEFAULT. +-- ERROR ALTER TABLE sales_range SPLIT PARTITION sales_others INTO (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), PARTITION sales_jan2022 FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'), @@ -385,9 +379,9 @@ SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conre SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_mar2022'::regclass::oid ORDER BY conname COLLATE "C"; SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_apr2022'::regclass::oid ORDER BY conname COLLATE "C"; --- ERROR: new row for relation "sales_mar2022" violates check constraint "sales_range_sales_amount_check" +-- ERROR INSERT INTO sales_range VALUES (1, 0, '2022-03-11'); --- ERROR: insert or update on table "sales_mar2022" violates foreign key constraint "sales_range_salesperson_id_fkey" +-- ERROR INSERT INTO sales_range VALUES (-1, 10, '2022-03-11'); -- ok INSERT INTO sales_range VALUES (1, 10, '2022-03-11'); @@ -430,7 +424,7 @@ ALTER TABLE salespeople SPLIT PARTITION salespeople10_40 INTO SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; --- ERROR: insert or update on table "sales" violates foreign key constraint "sales_salesperson_id_fkey" +-- ERROR INSERT INTO sales VALUES (40, 50, '2022-03-04'); -- ok INSERT INTO sales VALUES (30, 50, '2022-03-04'); @@ -608,31 +602,31 @@ CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Pete CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Beijing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok'); CREATE TABLE sales_others PARTITION OF sales_list DEFAULT; --- ERROR: new partition "sales_east" would overlap with another (not split) partition "sales_nord" +-- ERROR ALTER TABLE sales_list SPLIT PARTITION sales_all INTO (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok', 'Helsinki'), PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); --- ERROR: new partition "sales_west" would overlap with another new partition "sales_central" +-- ERROR ALTER TABLE sales_list SPLIT PARTITION sales_all INTO (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Lisbon', 'Kyiv')); --- ERROR: new partition "sales_west" cannot have NULL value because split partition "sales_all" does not have +-- ERROR ALTER TABLE sales_list SPLIT PARTITION sales_all INTO (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', NULL), PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); --- ERROR: new partition "sales_west" cannot have this value because split partition "sales_all" does not have +-- ERROR ALTER TABLE sales_list SPLIT PARTITION sales_all INTO (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Melbourne'), PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); --- ERROR: new partition cannot be DEFAULT because DEFAULT partition "sales_others" already exists +-- ERROR ALTER TABLE sales_list SPLIT PARTITION sales_all INTO (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Melbourne'), PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), @@ -644,7 +638,7 @@ DROP TABLE sales_list; -- Test for non-symbolic comparison of values (numeric values '0' and '0.0' are equal). CREATE TABLE t (a numeric) PARTITION BY LIST (a); CREATE TABLE t1 PARTITION OF t FOR VALUES in ('0', '1'); --- ERROR: new partition "x" would overlap with another new partition "x1" +-- ERROR ALTER TABLE t SPLIT PARTITION t1 INTO (PARTITION x FOR VALUES IN ('0'), PARTITION x1 FOR VALUES IN ('0.0', '1')); @@ -660,21 +654,19 @@ CREATE TABLE sales_list(sales_state VARCHAR(20)) PARTITION BY LIST (sales_state) CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Helsinki', 'St. Petersburg', 'Oslo'); CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Beijing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok', NULL); --- ERROR: new partitions combined partition bounds do not contain value (NULL) but split partition "sales_all" does --- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +-- ERROR ALTER TABLE sales_list SPLIT PARTITION sales_all INTO (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); --- ERROR: new partitions combined partition bounds do not contain value ('Kyiv'::character varying(20)) but split partition "sales_all" does --- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +-- ERROR ALTER TABLE sales_list SPLIT PARTITION sales_all INTO (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', NULL)); --- ERROR DEFAULT partition should be one +-- ERROR ALTER TABLE sales_list SPLIT PARTITION sales_all INTO (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), @@ -849,7 +841,7 @@ CREATE TABLE t1(i int, t text) PARTITION BY LIST (t); CREATE TABLE t1pa PARTITION OF t1 FOR VALUES IN ('A'); CREATE TABLE t2 (i int, t text) PARTITION BY RANGE (t); --- ERROR: relation "t1pa" is not a partition of relation "t2" +-- ERROR ALTER TABLE t2 SPLIT PARTITION t1pa INTO (PARTITION t2a FOR VALUES FROM ('A') TO ('B'), PARTITION t2b FOR VALUES FROM ('B') TO ('C')); @@ -868,7 +860,7 @@ SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid) WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text COLLATE "C"; --- ERROR: cannot create a permanent relation as partition of temporary relation "t" +-- ERROR ALTER TABLE t SPLIT PARTITION tp_0_2 INTO (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); @@ -1033,12 +1025,12 @@ CREATE TABLE t (i int) PARTITION BY HASH(i); CREATE TABLE tp1 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 0); CREATE TABLE tp2 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 1); --- ERROR: partition of hash-partitioned table cannot be split +-- ERROR ALTER TABLE t SPLIT PARTITION tp1 INTO (PARTITION tp1_1 FOR VALUES WITH (MODULUS 4, REMAINDER 0), PARTITION tp1_2 FOR VALUES WITH (MODULUS 4, REMAINDER 2)); --- ERROR: list of new partitions should contain at least two partitions +-- ERROR ALTER TABLE t SPLIT PARTITION tp1 INTO (PARTITION tp1_1 FOR VALUES WITH (MODULUS 4, REMAINDER 0)); diff --git a/postgres/regression_suite/sqljson.sql b/postgres/regression_suite/sqljson.sql index 22242aa2a..88bd0c835 100644 --- a/postgres/regression_suite/sqljson.sql +++ b/postgres/regression_suite/sqljson.sql @@ -213,6 +213,14 @@ WHERE JSON_ARRAY( RETURNING jsonb ) = '[]'::jsonb; +-- JSON_ARRAY(subquery) RETURNING with a length-restricted output type +-- Should fail +SELECT JSON_ARRAY(SELECT 1 RETURNING varchar(1)); +SELECT JSON_ARRAY(SELECT 1 WHERE FALSE RETURNING varchar(1)); +-- Should work +SELECT JSON_ARRAY(SELECT 1 RETURNING varchar(3)); +SELECT JSON_ARRAY(SELECT 1 WHERE FALSE RETURNING varchar(2)); + -- Should fail SELECT JSON_ARRAY(SELECT FROM (VALUES (1)) foo(i)); SELECT JSON_ARRAY(SELECT i, i FROM (VALUES (1)) foo(i)); diff --git a/postgres/regression_suite/strings.sql b/postgres/regression_suite/strings.sql index 01163ddc9..c37c83825 100644 --- a/postgres/regression_suite/strings.sql +++ b/postgres/regression_suite/strings.sql @@ -39,8 +39,8 @@ SELECT U&'wrong: \+2FFFFF'; -- while we're here, check the same cases in E-style literals SELECT E'd\u0061t\U00000061' AS "data"; SELECT E'a\\b' AS "a\b"; -SELECT E'wrong: \u061'; -SELECT E'wrong: \U0061'; +-- SELECT E'wrong: \u061'; +-- SELECT E'wrong: \U0061'; SELECT E'wrong: \udb99'; SELECT E'wrong: \udb99xy'; SELECT E'wrong: \udb99\\'; diff --git a/postgres/regression_suite/triggers.sql b/postgres/regression_suite/triggers.sql index e86f65cde..b7dc196df 100644 --- a/postgres/regression_suite/triggers.sql +++ b/postgres/regression_suite/triggers.sql @@ -2788,3 +2788,26 @@ drop table defer_trig; drop function whoami(); drop role regress_fn_owner; drop role regress_caller; + +-- +-- Test a recursive AFTER ROW trigger that nests after-trigger query levels +-- deeply enough to grow query_stack mid-fire. Outer levels then resume their +-- post-loop cleanup against the relocated stack. +-- +create table trigger_recursive (id int); +create function trigger_recursive_fn() returns trigger language plpgsql as $$ +begin + if new.id < 10 then + insert into trigger_recursive values (new.id + 1); + end if; + return new; +end$$; + +create trigger trigger_recursive after insert on trigger_recursive + for each row execute function trigger_recursive_fn(); + +insert into trigger_recursive values (1); +select count(*) from trigger_recursive; + +drop table trigger_recursive; +drop function trigger_recursive_fn(); diff --git a/squawk-vscode/src/extension.ts b/squawk-vscode/src/extension.ts index 9c5000e4e..aea0b4100 100644 --- a/squawk-vscode/src/extension.ts +++ b/squawk-vscode/src/extension.ts @@ -376,7 +376,7 @@ class SyntaxTreeProvider implements vscode.TextDocumentContentProvider { _onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) { if (isSqlDocument(event.document)) { - // via rust-analzyer: + // via rust-analyzer: // We need to order this after language server updates, but there's no API for that. // Hence, good old sleep(). void sleep(10).then(() => this._eventEmitter.fire(this._uri)) From 22685c127a3209d0fdb36e782f11ca8fbc7fa5ed Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Fri, 8 May 2026 18:37:03 -0400 Subject: [PATCH 2/2] fix --- crates/xtask/src/sync_pg.rs | 7 +++++++ postgres/regression_suite/strings.sql | 14 +++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/xtask/src/sync_pg.rs b/crates/xtask/src/sync_pg.rs index d32ea3070..28b3e1244 100644 --- a/crates/xtask/src/sync_pg.rs +++ b/crates/xtask/src/sync_pg.rs @@ -94,6 +94,13 @@ const IGNORED_LINES: &[&str] = &[ "REPACK (CONCURRENTLY) :toast_rel;", r#"SELECT E'wrong: \u061';"#, r#"SELECT E'wrong: \U0061';"#, + r#"SELECT U&'wrong: \061';"#, + r#"SELECT U&'wrong: \+0061';"#, + r#"SELECT U&'wrong: \db99';"#, + r#"SELECT U&'wrong: \db99xy';"#, + r#"SELECT U&'wrong: \db99\0061';"#, + r#"SELECT U&'wrong: \+00db99\+000061';"#, + r#"SELECT U&'wrong: \+2FFFFF';"#, r#"SELECT x'0 ';"#, r#"SELECT x' 0';"#, r#"SELECT b' 0';"#, diff --git a/postgres/regression_suite/strings.sql b/postgres/regression_suite/strings.sql index c37c83825..fde536729 100644 --- a/postgres/regression_suite/strings.sql +++ b/postgres/regression_suite/strings.sql @@ -24,17 +24,17 @@ SELECT U&'a\\b' AS "a\b"; SELECT U&' \' UESCAPE '!' AS "tricky"; SELECT 'tricky' AS U&"\" UESCAPE '!'; -SELECT U&'wrong: \061'; -SELECT U&'wrong: \+0061'; +-- SELECT U&'wrong: \061'; +-- SELECT U&'wrong: \+0061'; -- SELECT U&'wrong: +0061' UESCAPE +; SELECT U&'wrong: +0061' UESCAPE '+'; -SELECT U&'wrong: \db99'; -SELECT U&'wrong: \db99xy'; +-- SELECT U&'wrong: \db99'; +-- SELECT U&'wrong: \db99xy'; SELECT U&'wrong: \db99\\'; -SELECT U&'wrong: \db99\0061'; -SELECT U&'wrong: \+00db99\+000061'; -SELECT U&'wrong: \+2FFFFF'; +-- SELECT U&'wrong: \db99\0061'; +-- SELECT U&'wrong: \+00db99\+000061'; +-- SELECT U&'wrong: \+2FFFFF'; -- while we're here, check the same cases in E-style literals SELECT E'd\u0061t\U00000061' AS "data";