From 7c949e98cd4809567568d7d9a941364b75f34f9d Mon Sep 17 00:00:00 2001 From: Tyler White <50381805+IndexSeek@users.noreply.github.com> Date: Sat, 2 Aug 2025 21:54:01 -0400 Subject: [PATCH 1/7] Postgres: enhance NUMERIC/DECIMAL parsing to support negative scale values --- src/ast/data_type.rs | 2 +- src/parser/mod.rs | 105 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index b4a8af60f..dde9e0b7e 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -962,7 +962,7 @@ pub enum ExactNumberInfo { /// Only precision information, e.g. `DECIMAL(10)` Precision(u64), /// Precision and scale information, e.g. `DECIMAL(10,2)` - PrecisionAndScale(u64, u64), + PrecisionAndScale(u64, i64), } impl fmt::Display for ExactNumberInfo { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c3230a215..92ebb416f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11213,7 +11213,7 @@ impl<'a> Parser<'a> { if self.consume_token(&Token::LParen) { let precision = self.parse_literal_uint()?; let scale = if self.consume_token(&Token::Comma) { - Some(self.parse_literal_uint()?) + Some(self.parse_scale_value()?) } else { None }; @@ -11229,6 +11229,38 @@ impl<'a> Parser<'a> { } } + /// Parse a scale value for NUMERIC/DECIMAL data types. + /// + /// Supports positive, negative, and explicitly positive (with `+`) scale values. + /// Negative scale values are particularly useful for PostgreSQL, where they indicate + /// rounding to the left of the decimal point. For example: + /// - `NUMERIC(5, 2)` stores up to 5 digits with 2 decimal places (e.g., 123.45) + /// - `NUMERIC(5, -2)` stores up to 5 digits rounded to hundreds (e.g., 12300) + fn parse_scale_value(&mut self) -> Result { + let next_token = self.next_token(); + match next_token.token { + Token::Number(s, _) => Self::parse::(s, next_token.span.start), + Token::Minus => { + let next_token = self.next_token(); + match next_token.token { + Token::Number(s, _) => { + let positive_value = Self::parse::(s, next_token.span.start)?; + Ok(-positive_value) + } + _ => self.expected("number after minus", next_token), + } + } + Token::Plus => { + let next_token = self.next_token(); + match next_token.token { + Token::Number(s, _) => Self::parse::(s, next_token.span.start), + _ => self.expected("number after plus", next_token), + } + } + _ => self.expected("number", next_token), + } + } + pub fn parse_optional_type_modifiers(&mut self) -> Result>, ParserError> { if self.consume_token(&Token::LParen) { let mut modifiers = Vec::new(); @@ -17069,7 +17101,7 @@ mod tests { use crate::ast::{ CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, ObjectName, TimezoneInfo, }; - use crate::dialect::{AnsiDialect, GenericDialect}; + use crate::dialect::{AnsiDialect, GenericDialect, PostgreSqlDialect}; use crate::test_utils::TestedDialects; macro_rules! test_parse_data_type { @@ -17319,6 +17351,75 @@ mod tests { "DEC(2,10)", DataType::Dec(ExactNumberInfo::PrecisionAndScale(2, 10)) ); + + // Test negative scale values (PostgreSQL supports scale from -1000 to 1000) + test_parse_data_type!( + dialect, + "NUMERIC(10,-2)", + DataType::Numeric(ExactNumberInfo::PrecisionAndScale(10, -2)) + ); + + test_parse_data_type!( + dialect, + "DECIMAL(1000,-10)", + DataType::Decimal(ExactNumberInfo::PrecisionAndScale(1000, -10)) + ); + + test_parse_data_type!( + dialect, + "DEC(5,-1000)", + DataType::Dec(ExactNumberInfo::PrecisionAndScale(5, -1000)) + ); + + // Test positive scale with explicit plus sign + dialect.run_parser_method("NUMERIC(10,+5)", |parser| { + let data_type = parser.parse_data_type().unwrap(); + assert_eq!( + DataType::Numeric(ExactNumberInfo::PrecisionAndScale(10, 5)), + data_type + ); + // Note: Explicit '+' sign is not preserved in output, which is correct + assert_eq!("NUMERIC(10,5)", data_type.to_string()); + }); + } + + #[test] + fn test_numeric_negative_scale() { + let dialect = TestedDialects::new(vec![ + Box::new(PostgreSqlDialect {}), + Box::new(GenericDialect {}), + ]); + + // Test NUMERIC with negative scale + test_parse_data_type!( + dialect, + "NUMERIC(10,-5)", + DataType::Numeric(ExactNumberInfo::PrecisionAndScale(10, -5)) + ); + + // Test DECIMAL with negative scale + test_parse_data_type!( + dialect, + "DECIMAL(20,-10)", + DataType::Decimal(ExactNumberInfo::PrecisionAndScale(20, -10)) + ); + + // Test DEC with negative scale + test_parse_data_type!( + dialect, + "DEC(5,-2)", + DataType::Dec(ExactNumberInfo::PrecisionAndScale(5, -2)) + ); + + // Test with explicit positive scale (note: +5 parses as 5, so display shows NUMERIC(10,5)) + dialect.run_parser_method("NUMERIC(10,+5)", |parser| { + let data_type = parser.parse_data_type().unwrap(); + assert_eq!( + DataType::Numeric(ExactNumberInfo::PrecisionAndScale(10, 5)), + data_type + ); + assert_eq!("NUMERIC(10,5)", data_type.to_string()); + }); } #[test] From a6ae8a322fbe10637fcc3093c4869e89859bf969 Mon Sep 17 00:00:00 2001 From: Tyler White <50381805+IndexSeek@users.noreply.github.com> Date: Sun, 3 Aug 2025 07:57:48 -0400 Subject: [PATCH 2/7] docs: reduce parse_signed integer commenting Co-authored-by: Ifeanyi Ubah --- src/parser/mod.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 92ebb416f..03570319c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11229,14 +11229,8 @@ impl<'a> Parser<'a> { } } - /// Parse a scale value for NUMERIC/DECIMAL data types. - /// - /// Supports positive, negative, and explicitly positive (with `+`) scale values. - /// Negative scale values are particularly useful for PostgreSQL, where they indicate - /// rounding to the left of the decimal point. For example: - /// - `NUMERIC(5, 2)` stores up to 5 digits with 2 decimal places (e.g., 123.45) - /// - `NUMERIC(5, -2)` stores up to 5 digits rounded to hundreds (e.g., 12300) - fn parse_scale_value(&mut self) -> Result { + /// Parse an optionally signed integer literal. + fn parse_signed_integer(&mut self) -> Result { let next_token = self.next_token(); match next_token.token { Token::Number(s, _) => Self::parse::(s, next_token.span.start), From da3c38482526a253642e64ac48dc527caff8b05e Mon Sep 17 00:00:00 2001 From: Tyler White <50381805+IndexSeek@users.noreply.github.com> Date: Sun, 3 Aug 2025 07:59:16 -0400 Subject: [PATCH 3/7] docs: remove dialect callout Co-authored-by: Ifeanyi Ubah --- src/parser/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 03570319c..ebb3c8b0c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17346,7 +17346,7 @@ mod tests { DataType::Dec(ExactNumberInfo::PrecisionAndScale(2, 10)) ); - // Test negative scale values (PostgreSQL supports scale from -1000 to 1000) + // Test negative scale values. test_parse_data_type!( dialect, "NUMERIC(10,-2)", From 19d163d762b4df5dc90960c1256341b8cb906502 Mon Sep 17 00:00:00 2001 From: Tyler White <50381805+IndexSeek@users.noreply.github.com> Date: Sun, 3 Aug 2025 08:05:03 -0400 Subject: [PATCH 4/7] feat: simplify parse_signed_integer implementation --- src/parser/mod.rs | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ebb3c8b0c..c0769f5aa 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11232,26 +11232,24 @@ impl<'a> Parser<'a> { /// Parse an optionally signed integer literal. fn parse_signed_integer(&mut self) -> Result { let next_token = self.next_token(); - match next_token.token { - Token::Number(s, _) => Self::parse::(s, next_token.span.start), + let (sign, number_token) = match next_token.token { Token::Minus => { - let next_token = self.next_token(); - match next_token.token { - Token::Number(s, _) => { - let positive_value = Self::parse::(s, next_token.span.start)?; - Ok(-positive_value) - } - _ => self.expected("number after minus", next_token), - } + let number_token = self.next_token(); + (-1, number_token) } Token::Plus => { - let next_token = self.next_token(); - match next_token.token { - Token::Number(s, _) => Self::parse::(s, next_token.span.start), - _ => self.expected("number after plus", next_token), - } + let number_token = self.next_token(); + (1, number_token) + } + _ => (1, next_token), + }; + + match number_token.token { + Token::Number(s, _) => { + let value = Self::parse::(s, number_token.span.start)?; + Ok(sign * value) } - _ => self.expected("number", next_token), + _ => self.expected("number", number_token), } } From d31b3021bda517c92f717754430cb12ef2378705 Mon Sep 17 00:00:00 2001 From: Tyler White <50381805+IndexSeek@users.noreply.github.com> Date: Sun, 3 Aug 2025 08:22:50 -0400 Subject: [PATCH 5/7] test: merge negative scale tests --- src/parser/mod.rs | 35 +++++++++-------------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c0769f5aa..7093219bf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11213,7 +11213,7 @@ impl<'a> Parser<'a> { if self.consume_token(&Token::LParen) { let precision = self.parse_literal_uint()?; let scale = if self.consume_token(&Token::Comma) { - Some(self.parse_scale_value()?) + Some(self.parse_signed_integer()?) } else { None }; @@ -17299,8 +17299,11 @@ mod tests { #[test] fn test_ansii_exact_numeric_types() { // Exact numeric types: - let dialect = - TestedDialects::new(vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})]); + let dialect = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(AnsiDialect {}), + Box::new(PostgreSqlDialect {}), + ]); test_parse_data_type!(dialect, "NUMERIC", DataType::Numeric(ExactNumberInfo::None)); @@ -17363,53 +17366,33 @@ mod tests { DataType::Dec(ExactNumberInfo::PrecisionAndScale(5, -1000)) ); - // Test positive scale with explicit plus sign - dialect.run_parser_method("NUMERIC(10,+5)", |parser| { - let data_type = parser.parse_data_type().unwrap(); - assert_eq!( - DataType::Numeric(ExactNumberInfo::PrecisionAndScale(10, 5)), - data_type - ); - // Note: Explicit '+' sign is not preserved in output, which is correct - assert_eq!("NUMERIC(10,5)", data_type.to_string()); - }); - } - - #[test] - fn test_numeric_negative_scale() { - let dialect = TestedDialects::new(vec![ - Box::new(PostgreSqlDialect {}), - Box::new(GenericDialect {}), - ]); - - // Test NUMERIC with negative scale + // Additional negative scale test cases test_parse_data_type!( dialect, "NUMERIC(10,-5)", DataType::Numeric(ExactNumberInfo::PrecisionAndScale(10, -5)) ); - // Test DECIMAL with negative scale test_parse_data_type!( dialect, "DECIMAL(20,-10)", DataType::Decimal(ExactNumberInfo::PrecisionAndScale(20, -10)) ); - // Test DEC with negative scale test_parse_data_type!( dialect, "DEC(5,-2)", DataType::Dec(ExactNumberInfo::PrecisionAndScale(5, -2)) ); - // Test with explicit positive scale (note: +5 parses as 5, so display shows NUMERIC(10,5)) + // Test positive scale with explicit plus sign dialect.run_parser_method("NUMERIC(10,+5)", |parser| { let data_type = parser.parse_data_type().unwrap(); assert_eq!( DataType::Numeric(ExactNumberInfo::PrecisionAndScale(10, 5)), data_type ); + // Note: Explicit '+' sign is not preserved in output, which is correct assert_eq!("NUMERIC(10,5)", data_type.to_string()); }); } From 427988ec29fe1de28cf6e8a4b3231c635f842c7e Mon Sep 17 00:00:00 2001 From: Tyler White <50381805+IndexSeek@users.noreply.github.com> Date: Mon, 4 Aug 2025 19:44:42 -0400 Subject: [PATCH 6/7] feat: implement improved parsing for signed integers --- src/parser/mod.rs | 37 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7093219bf..f10181baf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11231,25 +11231,19 @@ impl<'a> Parser<'a> { /// Parse an optionally signed integer literal. fn parse_signed_integer(&mut self) -> Result { - let next_token = self.next_token(); - let (sign, number_token) = match next_token.token { - Token::Minus => { - let number_token = self.next_token(); - (-1, number_token) - } - Token::Plus => { - let number_token = self.next_token(); - (1, number_token) - } - _ => (1, next_token), - }; + if !self.consume_token(&Token::Minus) { + return i64::try_from(self.parse_literal_uint()?) + .map_err(|_| ParserError::ParserError("Integer overflow".to_string())); + } - match number_token.token { + self.advance_token(); + let next_token = self.get_current_token(); + match &next_token.token { Token::Number(s, _) => { - let value = Self::parse::(s, number_token.span.start)?; - Ok(sign * value) + let positive_value = Self::parse::(s.clone(), next_token.span.start)?; + Ok(-positive_value) } - _ => self.expected("number", number_token), + _ => self.expected_ref("literal int", next_token), } } @@ -17384,17 +17378,6 @@ mod tests { "DEC(5,-2)", DataType::Dec(ExactNumberInfo::PrecisionAndScale(5, -2)) ); - - // Test positive scale with explicit plus sign - dialect.run_parser_method("NUMERIC(10,+5)", |parser| { - let data_type = parser.parse_data_type().unwrap(); - assert_eq!( - DataType::Numeric(ExactNumberInfo::PrecisionAndScale(10, 5)), - data_type - ); - // Note: Explicit '+' sign is not preserved in output, which is correct - assert_eq!("NUMERIC(10,5)", data_type.to_string()); - }); } #[test] From 1c9ad37f0a234c3a933b168fc10defcd445bc369 Mon Sep 17 00:00:00 2001 From: Tyler White <50381805+IndexSeek@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:46:00 -0400 Subject: [PATCH 7/7] feat: enhance signed integer parsing to handle explicit '+' sign --- src/parser/mod.rs | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f10181baf..2f797eb80 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11231,19 +11231,22 @@ impl<'a> Parser<'a> { /// Parse an optionally signed integer literal. fn parse_signed_integer(&mut self) -> Result { - if !self.consume_token(&Token::Minus) { - return i64::try_from(self.parse_literal_uint()?) - .map_err(|_| ParserError::ParserError("Integer overflow".to_string())); + let is_negative = self.consume_token(&Token::Minus); + + if !is_negative { + let _ = self.consume_token(&Token::Plus); } - self.advance_token(); - let next_token = self.get_current_token(); - match &next_token.token { + let current_token = self.peek_token_ref(); + match ¤t_token.token { Token::Number(s, _) => { - let positive_value = Self::parse::(s.clone(), next_token.span.start)?; - Ok(-positive_value) + let s = s.clone(); + let span_start = current_token.span.start; + self.advance_token(); + let value = Self::parse::(s, span_start)?; + Ok(if is_negative { -value } else { value }) } - _ => self.expected_ref("literal int", next_token), + _ => self.expected_ref("number", current_token), } } @@ -17360,7 +17363,6 @@ mod tests { DataType::Dec(ExactNumberInfo::PrecisionAndScale(5, -1000)) ); - // Additional negative scale test cases test_parse_data_type!( dialect, "NUMERIC(10,-5)", @@ -17378,6 +17380,16 @@ mod tests { "DEC(5,-2)", DataType::Dec(ExactNumberInfo::PrecisionAndScale(5, -2)) ); + + dialect.run_parser_method("NUMERIC(10,+5)", |parser| { + let data_type = parser.parse_data_type().unwrap(); + assert_eq!( + DataType::Numeric(ExactNumberInfo::PrecisionAndScale(10, 5)), + data_type + ); + // Note: Explicit '+' sign is not preserved in output, which is correct + assert_eq!("NUMERIC(10,5)", data_type.to_string()); + }); } #[test]