diff --git a/crates/squawk_lexer/src/lib.rs b/crates/squawk_lexer/src/lib.rs index 8f92513da..4c41543fc 100644 --- a/crates/squawk_lexer/src/lib.rs +++ b/crates/squawk_lexer/src/lib.rs @@ -116,7 +116,11 @@ impl Cursor<'_> { while self.first().is_ascii_digit() { self.bump(); } - TokenKind::PositionalParam + let trailing_junk_start = self.pos_within_token(); + self.eat_identifier(); + TokenKind::PositionalParam { + trailing_junk_start, + } } } '`' => TokenKind::Backtick, @@ -240,9 +244,12 @@ impl Cursor<'_> { base = Base::Binary; self.bump(); if !self.eat_decimal_digits() { + let trailing_junk_start = self.pos_within_token(); + self.eat_identifier(); return LiteralKind::Int { base, empty_int: true, + trailing_junk_start, }; } } @@ -251,9 +258,12 @@ impl Cursor<'_> { base = Base::Octal; self.bump(); if !self.eat_decimal_digits() { + let trailing_junk_start = self.pos_within_token(); + self.eat_identifier(); return LiteralKind::Int { base, empty_int: true, + trailing_junk_start, }; } } @@ -262,9 +272,12 @@ impl Cursor<'_> { base = Base::Hexadecimal; self.bump(); if !self.eat_hexadecimal_digits() { + let trailing_junk_start = self.pos_within_token(); + self.eat_identifier(); return LiteralKind::Int { base, empty_int: true, + trailing_junk_start, }; } } @@ -278,9 +291,12 @@ impl Cursor<'_> { // Just a 0. _ => { + let trailing_junk_start = self.pos_within_token(); + self.eat_identifier(); return LiteralKind::Int { base, empty_int: false, + trailing_junk_start, }; } } @@ -313,23 +329,34 @@ impl Cursor<'_> { _ => (), } } + let trailing_junk_start = self.pos_within_token(); + self.eat_identifier(); LiteralKind::Float { base, empty_exponent, + trailing_junk_start, } } 'e' | 'E' => { self.bump(); let empty_exponent = !self.eat_float_exponent(); + let trailing_junk_start = self.pos_within_token(); + self.eat_identifier(); LiteralKind::Float { base, empty_exponent, + trailing_junk_start, + } + } + _ => { + let trailing_junk_start = self.pos_within_token(); + self.eat_identifier(); + LiteralKind::Int { + base, + empty_int: false, + trailing_junk_start, } } - _ => LiteralKind::Int { - base, - empty_int: false, - }, } } @@ -452,7 +479,7 @@ impl Cursor<'_> { let mut has_digits = false; loop { match self.first() { - '_' => { + '_' if self.second().is_ascii_digit() => { self.bump(); } '0'..='9' => { @@ -469,7 +496,7 @@ impl Cursor<'_> { let mut has_digits = false; loop { match self.first() { - '_' => { + '_' if self.second().is_ascii_hexdigit() => { self.bump(); } '0'..='9' | 'a'..='f' | 'A'..='F' => { @@ -485,11 +512,20 @@ impl Cursor<'_> { /// Eats the float exponent. Returns true if at least one digit was met, /// and returns false otherwise. fn eat_float_exponent(&mut self) -> bool { + if self.first() == '_' { + return false; + } if self.first() == '-' || self.first() == '+' { self.bump(); } self.eat_decimal_digits() } + + fn eat_identifier(&mut self) { + if is_ident_start(self.first()) { + self.eat_while(is_ident_cont); + } + } } /// Creates an iterator that produces tokens from the input string. diff --git a/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__lex_statement.snap b/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__lex_statement.snap index a76c42ba9..aabe9de6e 100644 --- a/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__lex_statement.snap +++ b/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__lex_statement.snap @@ -5,6 +5,6 @@ expression: result [ "select" @ Ident, " " @ Whitespace, - "1" @ Literal { kind: Int { base: Decimal, empty_int: false } }, + "1" @ Literal { kind: Int { base: Decimal, empty_int: false, trailing_junk_start: 1 } }, ";" @ Semi, ] diff --git a/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__numeric.snap b/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__numeric.snap index 519cf3d94..addf6bb52 100644 --- a/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__numeric.snap +++ b/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__numeric.snap @@ -4,26 +4,26 @@ expression: "lex(r#\"\n42\n3.5\n4.\n.001\n.123e10\n5e2\n1.925e-3\n1e-10\n1e+10\n --- [ "\n" @ Whitespace, - "42" @ Literal { kind: Int { base: Decimal, empty_int: false } }, + "42" @ Literal { kind: Int { base: Decimal, empty_int: false, trailing_junk_start: 2 } }, "\n" @ Whitespace, - "3.5" @ Literal { kind: Float { base: Decimal, empty_exponent: false } }, + "3.5" @ Literal { kind: Float { base: Decimal, empty_exponent: false, trailing_junk_start: 3 } }, "\n" @ Whitespace, - "4." @ Literal { kind: Float { base: Decimal, empty_exponent: false } }, + "4." @ Literal { kind: Float { base: Decimal, empty_exponent: false, trailing_junk_start: 2 } }, "\n" @ Whitespace, - ".001" @ Literal { kind: Int { base: Decimal, empty_int: false } }, + ".001" @ Literal { kind: Int { base: Decimal, empty_int: false, trailing_junk_start: 4 } }, "\n" @ Whitespace, - ".123e10" @ Literal { kind: Float { base: Decimal, empty_exponent: false } }, + ".123e10" @ Literal { kind: Float { base: Decimal, empty_exponent: false, trailing_junk_start: 7 } }, "\n" @ Whitespace, - "5e2" @ Literal { kind: Float { base: Decimal, empty_exponent: false } }, + "5e2" @ Literal { kind: Float { base: Decimal, empty_exponent: false, trailing_junk_start: 3 } }, "\n" @ Whitespace, - "1.925e-3" @ Literal { kind: Float { base: Decimal, empty_exponent: false } }, + "1.925e-3" @ Literal { kind: Float { base: Decimal, empty_exponent: false, trailing_junk_start: 8 } }, "\n" @ Whitespace, - "1e-10" @ Literal { kind: Float { base: Decimal, empty_exponent: false } }, + "1e-10" @ Literal { kind: Float { base: Decimal, empty_exponent: false, trailing_junk_start: 5 } }, "\n" @ Whitespace, - "1e+10" @ Literal { kind: Float { base: Decimal, empty_exponent: false } }, + "1e+10" @ Literal { kind: Float { base: Decimal, empty_exponent: false, trailing_junk_start: 5 } }, "\n" @ Whitespace, - "1e10" @ Literal { kind: Float { base: Decimal, empty_exponent: false } }, + "1e10" @ Literal { kind: Float { base: Decimal, empty_exponent: false, trailing_junk_start: 4 } }, "\n" @ Whitespace, - "4664.E+5" @ Literal { kind: Float { base: Decimal, empty_exponent: false } }, + "4664.E+5" @ Literal { kind: Float { base: Decimal, empty_exponent: false, trailing_junk_start: 8 } }, "\n" @ Whitespace, ] diff --git a/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__numeric_non_decimal.snap b/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__numeric_non_decimal.snap index 5050265f5..6ec55318c 100644 --- a/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__numeric_non_decimal.snap +++ b/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__numeric_non_decimal.snap @@ -4,16 +4,16 @@ expression: "lex(r#\"\n0b100101\n0B10011001\n0o273\n0O755\n0x42f\n0XFFFF\n\"#)" --- [ "\n" @ Whitespace, - "0b100101" @ Literal { kind: Int { base: Binary, empty_int: false } }, + "0b100101" @ Literal { kind: Int { base: Binary, empty_int: false, trailing_junk_start: 8 } }, "\n" @ Whitespace, - "0B10011001" @ Literal { kind: Int { base: Binary, empty_int: false } }, + "0B10011001" @ Literal { kind: Int { base: Binary, empty_int: false, trailing_junk_start: 10 } }, "\n" @ Whitespace, - "0o273" @ Literal { kind: Int { base: Octal, empty_int: false } }, + "0o273" @ Literal { kind: Int { base: Octal, empty_int: false, trailing_junk_start: 5 } }, "\n" @ Whitespace, - "0O755" @ Literal { kind: Int { base: Octal, empty_int: false } }, + "0O755" @ Literal { kind: Int { base: Octal, empty_int: false, trailing_junk_start: 5 } }, "\n" @ Whitespace, - "0x42f" @ Literal { kind: Int { base: Hexadecimal, empty_int: false } }, + "0x42f" @ Literal { kind: Int { base: Hexadecimal, empty_int: false, trailing_junk_start: 5 } }, "\n" @ Whitespace, - "0XFFFF" @ Literal { kind: Int { base: Hexadecimal, empty_int: false } }, + "0XFFFF" @ Literal { kind: Int { base: Hexadecimal, empty_int: false, trailing_junk_start: 6 } }, "\n" @ Whitespace, ] diff --git a/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__numeric_with_seperators.snap b/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__numeric_with_seperators.snap index 46814c5f6..bfc095b89 100644 --- a/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__numeric_with_seperators.snap +++ b/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__numeric_with_seperators.snap @@ -4,14 +4,14 @@ expression: "lex(r#\"\n1_500_000_000\n0b10001000_00000000\n0o_1_755\n0xFFFF_FFFF --- [ "\n" @ Whitespace, - "1_500_000_000" @ Literal { kind: Int { base: Decimal, empty_int: false } }, + "1_500_000_000" @ Literal { kind: Int { base: Decimal, empty_int: false, trailing_junk_start: 13 } }, "\n" @ Whitespace, - "0b10001000_00000000" @ Literal { kind: Int { base: Binary, empty_int: false } }, + "0b10001000_00000000" @ Literal { kind: Int { base: Binary, empty_int: false, trailing_junk_start: 19 } }, "\n" @ Whitespace, - "0o_1_755" @ Literal { kind: Int { base: Octal, empty_int: false } }, + "0o_1_755" @ Literal { kind: Int { base: Octal, empty_int: false, trailing_junk_start: 8 } }, "\n" @ Whitespace, - "0xFFFF_FFFF" @ Literal { kind: Int { base: Hexadecimal, empty_int: false } }, + "0xFFFF_FFFF" @ Literal { kind: Int { base: Hexadecimal, empty_int: false, trailing_junk_start: 11 } }, "\n" @ Whitespace, - "1.618_034" @ Literal { kind: Float { base: Decimal, empty_exponent: false } }, + "1.618_034" @ Literal { kind: Float { base: Decimal, empty_exponent: false, trailing_junk_start: 9 } }, "\n" @ Whitespace, ] diff --git a/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__params.snap b/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__params.snap index 8ee456f48..2a5dd0afa 100644 --- a/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__params.snap +++ b/crates/squawk_lexer/src/snapshots/squawk_lexer__tests__params.snap @@ -6,21 +6,21 @@ expression: "lex(r#\"\nselect $1 + $2;\n\nselect $1123123123123;\n\nselect $;\n\ "\n" @ Whitespace, "select" @ Ident, " " @ Whitespace, - "$1" @ PositionalParam, + "$1" @ PositionalParam { trailing_junk_start: 2 }, " " @ Whitespace, "+" @ Plus, " " @ Whitespace, - "$2" @ PositionalParam, + "$2" @ PositionalParam { trailing_junk_start: 2 }, ";" @ Semi, "\n\n" @ Whitespace, "select" @ Ident, " " @ Whitespace, - "$1123123123123" @ PositionalParam, + "$1123123123123" @ PositionalParam { trailing_junk_start: 14 }, ";" @ Semi, "\n\n" @ Whitespace, "select" @ Ident, " " @ Whitespace, - "$" @ PositionalParam, + "$" @ PositionalParam { trailing_junk_start: 1 }, ";" @ Semi, "\n" @ Whitespace, ] diff --git a/crates/squawk_lexer/src/token.rs b/crates/squawk_lexer/src/token.rs index 525e84a0d..bec1b5b13 100644 --- a/crates/squawk_lexer/src/token.rs +++ b/crates/squawk_lexer/src/token.rs @@ -84,7 +84,7 @@ pub enum TokenKind { /// Positional Parameter, e.g., `$1` /// /// see: - PositionalParam, + PositionalParam { trailing_junk_start: u32 }, /// Quoted Identifier, e.g., `"update"` in `update "my_table" set "a" = 5;` /// /// These are case-sensitive, unlike [`TokenKind::Ident`] @@ -127,11 +127,19 @@ pub enum LiteralKind { /// Integer Numeric, e.g., `42` /// /// see: - Int { base: Base, empty_int: bool }, + Int { + base: Base, + empty_int: bool, + trailing_junk_start: u32, + }, /// Float Numeric, e.g., `1.925e-3` /// /// see: - Float { base: Base, empty_exponent: bool }, + Float { + base: Base, + empty_exponent: bool, + trailing_junk_start: u32, + }, /// String, e.g., `'foo'` /// /// see: diff --git a/crates/squawk_parser/src/lexed_str.rs b/crates/squawk_parser/src/lexed_str.rs index 8d377622c..e2bced591 100644 --- a/crates/squawk_parser/src/lexed_str.rs +++ b/crates/squawk_parser/src/lexed_str.rs @@ -209,7 +209,14 @@ impl<'a> Converter<'a> { } squawk_lexer::TokenKind::Eof => SyntaxKind::EOF, squawk_lexer::TokenKind::Backtick => SyntaxKind::BACKTICK, - squawk_lexer::TokenKind::PositionalParam => SyntaxKind::POSITIONAL_PARAM, + squawk_lexer::TokenKind::PositionalParam { + trailing_junk_start, + } => { + if (*trailing_junk_start as usize) < token_text.len() { + err = "trailing junk after positional parameter"; + } + SyntaxKind::POSITIONAL_PARAM + } squawk_lexer::TokenKind::QuotedIdent { terminated } => { if !terminated { err = "Missing trailing \" to terminate the quoted identifier" @@ -227,18 +234,27 @@ impl<'a> Converter<'a> { let mut err: Option = None; let syntax_kind = match *kind { - squawk_lexer::LiteralKind::Int { empty_int, base: _ } => { + squawk_lexer::LiteralKind::Int { + empty_int, + base: _, + trailing_junk_start, + } => { if empty_int { err = Some("Missing digits after the integer base prefix".into()); + } else if (trailing_junk_start as usize) < token_text.len() { + err = Some("trailing junk after numeric literal".into()); } SyntaxKind::INT_NUMBER } squawk_lexer::LiteralKind::Float { empty_exponent, base: _, + trailing_junk_start, } => { if empty_exponent { err = Some("Missing digits after the exponent symbol".into()); + } else if (trailing_junk_start as usize) < token_text.len() { + err = Some("trailing junk after numeric literal".into()); } SyntaxKind::FLOAT_NUMBER } diff --git a/crates/squawk_parser/tests/data/err/select_literal.sql b/crates/squawk_parser/tests/data/err/select_literal.sql new file mode 100644 index 000000000..79b2f82ab --- /dev/null +++ b/crates/squawk_parser/tests/data/err/select_literal.sql @@ -0,0 +1,26 @@ +SELECT 123abc; +SELECT 0x0o; +SELECT 0.a; +SELECT 0.0a; +SELECT .0a; +SELECT 0.0e1a; +SELECT 0.0e; +SELECT 0.0e+a; +SELECT $1a; +SELECT 0b; +SELECT 1b; +SELECT 0b0x; +SELECT 0o; +SELECT 1o; +SELECT 0o0x; +SELECT 0x; +SELECT 1x; +SELECT 0x0y; +SELECT 100_; +SELECT 100__000; +SELECT _1_000.5; +SELECT 1_000_.5; +SELECT 1_000._5; +SELECT 1_000.5_; +SELECT 1_000.5e_1; +SELECT $0_1; diff --git a/crates/squawk_parser/tests/snapshots/tests__select_literal_err.snap b/crates/squawk_parser/tests/snapshots/tests__select_literal_err.snap new file mode 100644 index 000000000..0172bd97c --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__select_literal_err.snap @@ -0,0 +1,380 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: crates/squawk_parser/tests/data/err/select_literal.sql +--- +SOURCE_FILE + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "123abc" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "0x0o" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + FLOAT_NUMBER "0.a" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + FLOAT_NUMBER "0.0a" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER ".0a" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + FLOAT_NUMBER "0.0e1a" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + FLOAT_NUMBER "0.0e" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + FLOAT_NUMBER "0.0e+a" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + POSITIONAL_PARAM "$1a" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "0b" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "1b" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "0b0x" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "0o" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "1o" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "0o0x" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "0x" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "1x" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "0x0y" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "100_" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "100__000" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + NAME_REF + IDENT "_1_000" + TARGET + LITERAL + INT_NUMBER ".5" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "1_000_" + TARGET + LITERAL + INT_NUMBER ".5" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + FLOAT_NUMBER "1_000._5" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + FLOAT_NUMBER "1_000.5_" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + FLOAT_NUMBER "1_000.5e_1" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "SELECT" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + POSITIONAL_PARAM "$0_1" + SEMICOLON ";" + WHITESPACE "\n" +--- +error[syntax-error]: trailing junk after numeric literal + ╭▸ +1 │ SELECT 123abc; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +2 │ SELECT 0x0o; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +3 │ SELECT 0.a; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +4 │ SELECT 0.0a; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +5 │ SELECT .0a; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +6 │ SELECT 0.0e1a; + ╰╴ ━ +error[syntax-error]: Missing digits after the exponent symbol + ╭▸ +7 │ SELECT 0.0e; + ╰╴ ━ +error[syntax-error]: Missing digits after the exponent symbol + ╭▸ +8 │ SELECT 0.0e+a; + ╰╴ ━ +error[syntax-error]: trailing junk after positional parameter + ╭▸ +9 │ SELECT $1a; + ╰╴ ━ +error[syntax-error]: Missing digits after the integer base prefix + ╭▸ +10 │ SELECT 0b; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +11 │ SELECT 1b; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +12 │ SELECT 0b0x; + ╰╴ ━ +error[syntax-error]: Missing digits after the integer base prefix + ╭▸ +13 │ SELECT 0o; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +14 │ SELECT 1o; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +15 │ SELECT 0o0x; + ╰╴ ━ +error[syntax-error]: Missing digits after the integer base prefix + ╭▸ +16 │ SELECT 0x; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +17 │ SELECT 1x; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +18 │ SELECT 0x0y; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +19 │ SELECT 100_; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +20 │ SELECT 100__000; + ╰╴ ━ +error[syntax-error]: missing comma + ╭▸ +21 │ SELECT _1_000.5; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +22 │ SELECT 1_000_.5; + ╰╴ ━ +error[syntax-error]: missing comma + ╭▸ +22 │ SELECT 1_000_.5; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +23 │ SELECT 1_000._5; + ╰╴ ━ +error[syntax-error]: trailing junk after numeric literal + ╭▸ +24 │ SELECT 1_000.5_; + ╰╴ ━ +error[syntax-error]: Missing digits after the exponent symbol + ╭▸ +25 │ SELECT 1_000.5e_1; + ╰╴ ━ +error[syntax-error]: trailing junk after positional parameter + ╭▸ +26 │ SELECT $0_1; + ╰╴ ━ diff --git a/crates/xtask/src/sync_pg.rs b/crates/xtask/src/sync_pg.rs index 7223a18d9..58d61ebdb 100644 --- a/crates/xtask/src/sync_pg.rs +++ b/crates/xtask/src/sync_pg.rs @@ -35,12 +35,43 @@ const IGNORED_LINES: &[&str] = &[ "CREATE VIEW tableam_view_heap2 USING heap2 AS SELECT * FROM tableam_tbl_heap2;", "SELECT INTO tableam_tblselectinto_heap2 USING heap2 FROM tableam_tbl_heap2;", "INSERT INTO foo DEFAULT VALUES RETURNING WITH (nonsuch AS something) *;", + "SELECT _1_000.5;", + "SELECT 123abc;", + "SELECT 0x0o;", + "SELECT 0.a;", + "SELECT 0.0a;", + "SELECT .0a;", + "SELECT 0.0e1a;", "SELECT 0.0e;", "SELECT 0.0e+a;", + "PREPARE p1 AS SELECT $1a;", + "PREPARE p1 AS SELECT $2147483648;", "SELECT 0b;", + "SELECT 1b;", + "SELECT 0b0x;", "SELECT 0o;", + "SELECT 1o;", + "SELECT 0o0x;", "SELECT 0x;", - "SELECT _1_000.5;", + "SELECT 1x;", + "SELECT 0x0y;", + "SELECT 1_000_000;", + "SELECT 1_2_3;", + "SELECT 0x1EEE_FFFF;", + "SELECT 0o2_73;", + "SELECT 0b_10_0101;", + "SELECT 1_000.000_005;", + "SELECT 1_000.;", + "SELECT .000_005;", + "SELECT 1_000.5e0_1;", + "SELECT _100;", + "SELECT 100_;", + "SELECT 100__000;", + "SELECT 1_000_.5;", + "SELECT 1_000._5;", + "SELECT 1_000.5_;", + "SELECT 1_000.5e_1;", + "PREPARE p1 AS SELECT $0_1;", "EXPLAIN (COSTS OFF) :qry;", ":qry;", "create table foo (with baz);", diff --git a/postgres/kwlist.h b/postgres/kwlist.h index 170e24ded..f6061daa7 100644 --- a/postgres/kwlist.h +++ b/postgres/kwlist.h @@ -1,7 +1,7 @@ // synced from: -// commit: 901ed9b352b41f034e17bc540725082a488fce31 -// committed at: 2026-05-08T16:44:25+07:00 -// file: https://github.com/postgres/postgres/blob/901ed9b352b41f034e17bc540725082a488fce31/src/include/parser/kwlist.h +// commit: 0392fb900eb89f52988cccd33046443c39c70d1c +// committed at: 2026-05-20T23:23:49+03:00 +// file: https://github.com/postgres/postgres/blob/0392fb900eb89f52988cccd33046443c39c70d1c/src/include/parser/kwlist.h // // update via: // cargo xtask sync-pg diff --git a/postgres/regression_suite/compression_pglz.sql b/postgres/regression_suite/compression_pglz.sql index 981301764..682790e0f 100644 --- a/postgres/regression_suite/compression_pglz.sql +++ b/postgres/regression_suite/compression_pglz.sql @@ -48,6 +48,16 @@ SELECT test_pglz_decompress('\x01ff'::bytea, 1024, true); SELECT test_pglz_decompress('\x010f01'::bytea, 1024, false); SELECT test_pglz_decompress('\x010f01'::bytea, 1024, true); +-- Corrupted compressed data. Set control bit with a valid 2-byte match +-- tag where offset exceeds output written. +SELECT test_pglz_decompress('\x011001'::bytea, 1024, false); +SELECT test_pglz_decompress('\x011001'::bytea, 1024, true); + +-- Corrupted compressed data. Set control bit with a valid 2-byte match +-- tag where offset is 0. +SELECT test_pglz_decompress('\x010300'::bytea, 1024, false); +SELECT test_pglz_decompress('\x010300'::bytea, 1024, true); + -- Clean up DROP FUNCTION test_pglz_compress; DROP FUNCTION test_pglz_decompress; diff --git a/postgres/regression_suite/copy.sql b/postgres/regression_suite/copy.sql index d676eba4e..6e21f9cb7 100644 --- a/postgres/regression_suite/copy.sql +++ b/postgres/regression_suite/copy.sql @@ -535,3 +535,12 @@ CREATE TABLE pp_510 PARTITION OF pp_2 FOR VALUES FROM (5) TO (10); INSERT INTO pp SELECT g, 10 + g FROM generate_series(1,6) g; COPY pp TO stdout(header); DROP TABLE PP; + +-- Check if COPY TO handles dropped columns in partitions. +CREATE TABLE pp_dropcol (id int, val int) PARTITION BY RANGE (id); +CREATE TABLE pp_dropcol_1 (dropme int, id int, val int); +ALTER TABLE pp_dropcol_1 DROP COLUMN dropme; +ALTER TABLE pp_dropcol ATTACH PARTITION pp_dropcol_1 FOR VALUES FROM (1) TO (10); +INSERT INTO pp_dropcol VALUES (1, 11), (2, 12); +COPY pp_dropcol TO stdout(header); +DROP TABLE pp_dropcol; diff --git a/postgres/regression_suite/copy2.sql b/postgres/regression_suite/copy2.sql index 936ad0217..33b926548 100644 --- a/postgres/regression_suite/copy2.sql +++ b/postgres/regression_suite/copy2.sql @@ -555,6 +555,10 @@ COPY t_on_error_null FROM STDIN WITH (on_error set_null); -- fail -- \N 11 13 -- \. +COPY t_on_error_null(c, a) FROM STDIN WITH (on_error set_null); -- fail +-- 11 \N +-- \. + COPY t_on_error_null FROM STDIN WITH (on_error set_null); -- fail -- ss 11 14 -- \. diff --git a/postgres/regression_suite/jsonb_jsonpath.sql b/postgres/regression_suite/jsonb_jsonpath.sql index d3a38c577..c1f4ab542 100644 --- a/postgres/regression_suite/jsonb_jsonpath.sql +++ b/postgres/regression_suite/jsonb_jsonpath.sql @@ -721,6 +721,10 @@ select jsonb_path_query('"hello world"', '$.replace("hello","bye") starts with " -- Test .split_part() select jsonb_path_query('"abc~@~def~@~ghi"', '$.split_part("~@~", 2)'); select jsonb_path_query('"abc,def,ghi,jkl"', '$.split_part(",", -2)'); +select jsonb_path_query('"a,b"', '$.split_part(",", 0)'); +select jsonb_path_query('"a,b"', '$.split_part(",", 0)', silent => true); +select jsonb_path_query('"a,b"', '$.split_part(",", 2147483648)'); +select jsonb_path_query('"a,b"', '$.split_part(",", 2147483648)', silent => true); -- Test string methods play nicely together select jsonb_path_query('"hello world"', '$.replace("hello","bye").upper()'); diff --git a/postgres/regression_suite/multirangetypes.sql b/postgres/regression_suite/multirangetypes.sql index c715d830e..5038876c0 100644 --- a/postgres/regression_suite/multirangetypes.sql +++ b/postgres/regression_suite/multirangetypes.sql @@ -743,6 +743,22 @@ drop type textrange1; reset role; drop role regress_multirange_owner; +-- +-- CREATE TYPE checks for CREATE on multirange schema +-- +create role regress_mr; +create schema mr_sch; +set role regress_mr; +create type mytype as range (subtype=int4, multirange_type_name=mr_sch.mr_type); +reset role; +grant create on schema mr_sch to regress_mr; +set role regress_mr; +create type mytype as range (subtype=int4, multirange_type_name=mr_sch.mr_type); +reset role; +drop type mytype; +drop schema mr_sch; +drop role regress_mr; + -- -- Test polymorphic type system -- diff --git a/postgres/regression_suite/numerology.sql b/postgres/regression_suite/numerology.sql index 45e5e591d..1d86a9fe6 100644 --- a/postgres/regression_suite/numerology.sql +++ b/postgres/regression_suite/numerology.sql @@ -43,40 +43,40 @@ SELECT -0x8000000000000000; SELECT -0x8000000000000001; -- error cases -SELECT 123abc; -SELECT 0x0o; -SELECT 0.a; -SELECT 0.0a; -SELECT .0a; -SELECT 0.0e1a; +-- SELECT 123abc; +-- SELECT 0x0o; +-- SELECT 0.a; +-- SELECT 0.0a; +-- SELECT .0a; +-- SELECT 0.0e1a; -- SELECT 0.0e; -- SELECT 0.0e+a; -PREPARE p1 AS SELECT $1a; -PREPARE p1 AS SELECT $2147483648; +-- PREPARE p1 AS SELECT $1a; +-- PREPARE p1 AS SELECT $2147483648; -- SELECT 0b; -SELECT 1b; -SELECT 0b0x; +-- SELECT 1b; +-- SELECT 0b0x; -- SELECT 0o; -SELECT 1o; -SELECT 0o0x; +-- SELECT 1o; +-- SELECT 0o0x; -- SELECT 0x; -SELECT 1x; -SELECT 0x0y; +-- SELECT 1x; +-- SELECT 0x0y; -- underscores -SELECT 1_000_000; -SELECT 1_2_3; -SELECT 0x1EEE_FFFF; -SELECT 0o2_73; -SELECT 0b_10_0101; +-- SELECT 1_000_000; +-- SELECT 1_2_3; +-- SELECT 0x1EEE_FFFF; +-- SELECT 0o2_73; +-- SELECT 0b_10_0101; -SELECT 1_000.000_005; -SELECT 1_000.; -SELECT .000_005; -SELECT 1_000.5e0_1; +-- SELECT 1_000.000_005; +-- SELECT 1_000.; +-- SELECT .000_005; +-- SELECT 1_000.5e0_1; DO $$ DECLARE @@ -88,17 +88,17 @@ BEGIN END $$; -- error cases -SELECT _100; -SELECT 100_; -SELECT 100__000; +-- SELECT _100; +-- SELECT 100_; +-- SELECT 100__000; -- SELECT _1_000.5; -SELECT 1_000_.5; -SELECT 1_000._5; -SELECT 1_000.5_; -SELECT 1_000.5e_1; +-- SELECT 1_000_.5; +-- SELECT 1_000._5; +-- SELECT 1_000.5_; +-- SELECT 1_000.5e_1; -PREPARE p1 AS SELECT $0_1; +-- PREPARE p1 AS SELECT $0_1; -- -- Test implicit type conversions diff --git a/postgres/regression_suite/partition_split.sql b/postgres/regression_suite/partition_split.sql index ef98e09d5..e163b7b8f 100644 --- a/postgres/regression_suite/partition_split.sql +++ b/postgres/regression_suite/partition_split.sql @@ -834,6 +834,21 @@ SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text DROP TABLE sales_range; +-- +-- Test that the explicit partition bound cannot extend outside the split +-- partition's bound when a DEFAULT partition is specified. +-- +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_0_51 PARTITION OF t FOR VALUES FROM (0) TO (51); +CREATE TABLE tp_51_100 PARTITION OF t FOR VALUES FROM (51) TO (100); + +-- ERROR +ALTER TABLE t SPLIT PARTITION tp_0_51 INTO + (PARTITION tp_0_51 FOR VALUES FROM (0) TO (53), + PARTITION tp_default DEFAULT); + +DROP TABLE t; + -- -- Try to SPLIT partition of another table. -- diff --git a/postgres/regression_suite/stats_import.sql b/postgres/regression_suite/stats_import.sql index 0b5fb897a..3f023428a 100644 --- a/postgres/regression_suite/stats_import.sql +++ b/postgres/regression_suite/stats_import.sql @@ -645,6 +645,60 @@ AND tablename = 'test' AND inherited = false AND attname = 'id'; +-- warn: mcv / mcf array length mismatch (more vals), mcv-pair fails, rest get set +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'null_frac', 0.24::real, + 'most_common_vals', '{2,1,3}'::text, + 'most_common_freqs', '{0.3,0.25}'::real[] + ); + +SELECT * +FROM stats_import.pg_stats_stable +WHERE schemaname = 'stats_import' +AND tablename = 'test' +AND inherited = false +AND attname = 'id'; + +-- warn: mcv / mcf array length mismatch (more freqs), mcv-pair fails, rest get set +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'null_frac', 0.25::real, + 'most_common_vals', '{2,1}'::text, + 'most_common_freqs', '{0.3,0.25,0.05}'::real[] + ); + +SELECT * +FROM stats_import.pg_stats_stable +WHERE schemaname = 'stats_import' +AND tablename = 'test' +AND inherited = false +AND attname = 'id'; + +-- warn: most_common_vals is multi-dimensional, mcv-pair fails, rest get set +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'null_frac', 0.26::real, + 'most_common_vals', '{{2,1},{3,4}}'::text, + 'most_common_freqs', '{0.3,0.25,0.05,0.04}'::real[] + ); + +SELECT * +FROM stats_import.pg_stats_stable +WHERE schemaname = 'stats_import' +AND tablename = 'test' +AND inherited = false +AND attname = 'id'; + -- ok: mcv+mcf SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'stats_import', @@ -1696,7 +1750,7 @@ SELECT pg_catalog.pg_restore_extended_stats( 'statistics_name', 'test_stat_clone', 'inherited', false, 'exprs', '{ "avg_width": "4", "null_frac": "0" }'::jsonb); --- wrong number of exprs +-- wrong number of exprs, too few SELECT pg_catalog.pg_restore_extended_stats( 'schemaname', 'stats_import', 'relname', 'test_clone', @@ -1704,6 +1758,14 @@ SELECT pg_catalog.pg_restore_extended_stats( 'statistics_name', 'test_stat_clone', 'inherited', false, 'exprs', '[ { "avg_width": "4" } ]'::jsonb); +-- wrong number of exprs, too many +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ { "avg_width": "4" }, { "avg_width": "4" }, { "avg_width": "4" } ]'::jsonb); -- incorrect type of value: should be a string or a NULL. SELECT pg_catalog.pg_restore_extended_stats( 'schemaname', 'stats_import', @@ -1784,6 +1846,17 @@ SELECT pg_catalog.pg_restore_extended_stats( { "most_common_freqs": "{0.5}" }, { "most_common_vals": "{2}", "most_common_freqs": "{0.5}" } ]'::jsonb); +-- exprs most_common_vals is multi-dimensional +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { "most_common_vals": "{{1,2},{3,4}}", "most_common_freqs": "{0.3,0.25,0.05,0.04}" }, + { "most_common_vals": "{2}", "most_common_freqs": "{0.5}" } + ]'::jsonb); -- exprs most_common_vals element wrong type SELECT pg_catalog.pg_restore_extended_stats( 'schemaname', 'stats_import', @@ -1828,6 +1901,17 @@ SELECT pg_catalog.pg_restore_extended_stats( { "most_common_vals": "{1}", "most_common_freqs": "{BADMCF}" }, { "most_common_vals": "{2}", "most_common_freqs": "{0.5}" } ]'::jsonb); +-- exprs most_common_vals / most_common_freqs array length mismatch +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { "most_common_vals": "{1,3}", "most_common_freqs": "{0.5}" }, + { "most_common_vals": "{2}", "most_common_freqs": "{0.5}" } + ]'::jsonb); -- exprs histogram wrong type SELECT pg_catalog.pg_restore_extended_stats( 'schemaname', 'stats_import', @@ -2168,6 +2252,15 @@ WHERE e.statistics_schemaname = 'stats_import' AND e.inherited = false /* \gx */; +-- bad: exprs param which is a prefix of a valid key name +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[{ "n": "-1" }]'::jsonb); + -- ok: tsvector exceptions, test just the collation exceptions CREATE STATISTICS stats_import.test_stat_tsvec ON (length(name)), (to_tsvector(name)) FROM stats_import.test; SELECT pg_catalog.pg_restore_extended_stats( diff --git a/postgres/regression_suite/tsearch.sql b/postgres/regression_suite/tsearch.sql index abfd60feb..0a93455ff 100644 --- a/postgres/regression_suite/tsearch.sql +++ b/postgres/regression_suite/tsearch.sql @@ -647,6 +647,14 @@ SELECT ts_headline('english', SELECT ts_headline('english', 'foo bar', to_tsquery('english', '')); +-- Test for large values of StartSel, StopSel and FragmentDelimiter +SELECT ts_headline('english', 'foo barbar', to_tsquery('english', 'foo'), + 'StartSel=' || repeat('x', 32768)); +SELECT ts_headline('english', 'foo barbar', to_tsquery('english', 'foo'), + 'StopSel=' || repeat('x', 32768)); +SELECT ts_headline('english', 'foo barbar', to_tsquery('english', 'foo'), + 'FragmentDelimiter=' || repeat('x', 32768)); + --Rewrite sub system CREATE TABLE test_tsquery (txtkeyword TEXT, txtsample TEXT); diff --git a/postgres/regression_suite/updatable_views.sql b/postgres/regression_suite/updatable_views.sql index 0259f4fec..3f66647be 100644 --- a/postgres/regression_suite/updatable_views.sql +++ b/postgres/regression_suite/updatable_views.sql @@ -1903,6 +1903,17 @@ select * from uv_fpo_view order by id, valid_at; delete from uv_fpo_view for portion of valid_at from '2017-01-01' to '2022-01-01' where id = '[1,1]'; select * from uv_fpo_view order by id, valid_at; +-- UPDATE/DELETE FOR PORTION fails if the column is not updatable +-- (e.g. a computed expression, not a base column): +create view uv_fpo_view_nonupd as + select id, '[1,20]'::int4range as valid_at, b + from uv_fpo_tab; +-- Updating fails: +update uv_fpo_view_nonupd for portion of valid_at from 1 to 10 set b = 2; +-- Deleting fails: +delete from uv_fpo_view_nonupd for portion of valid_at from 1 to 10; +drop view uv_fpo_view_nonupd; + -- Test whole-row references to the view create table uv_iocu_tab (a int unique, b text); create view uv_iocu_view as diff --git a/postgres/regression_suite/window.sql b/postgres/regression_suite/window.sql index 999c7aecc..3dcb29f5d 100644 --- a/postgres/regression_suite/window.sql +++ b/postgres/regression_suite/window.sql @@ -2157,5 +2157,33 @@ SELECT x, FROM generate_series(1,5) g(x) WINDOW w AS (ORDER BY x ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING); +-- volatile arguments cannot use the IGNORE NULLS nullness cache +CREATE TEMPORARY SEQUENCE null_treatment_seq; +CREATE FUNCTION pg_temp.volatile_null(i int) RETURNS int +LANGUAGE sql VOLATILE AS +$$ + SELECT CASE WHEN nextval('null_treatment_seq') % 2 = 0 THEN i ELSE NULL END; +$$; + +SELECT x, + first_value(pg_temp.volatile_null(x)) IGNORE NULLS + OVER (ORDER BY x ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) +FROM generate_series(1,5) g(x); +SELECT last_value FROM null_treatment_seq; + +ALTER SEQUENCE null_treatment_seq RESTART WITH 1; +SELECT x, + lead(pg_temp.volatile_null(x), 1) IGNORE NULLS OVER (ORDER BY x) +FROM generate_series(1,5) g(x); +SELECT last_value FROM null_treatment_seq; + +ALTER SEQUENCE null_treatment_seq RESTART WITH 1; +SELECT x, + first_value((SELECT CASE WHEN nextval('null_treatment_seq') % 2 = 0 + THEN x ELSE NULL END)) IGNORE NULLS + OVER (ORDER BY x ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) +FROM generate_series(1,5) g(x); +SELECT last_value FROM null_treatment_seq; + --cleanup DROP TABLE planets CASCADE;