Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: replace proof-of-sql-parser with sqlparser #286

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions crates/proof-of-sql-parser/src/identifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ impl Identifier {
Self::from_str(string.as_ref())
}

/// An alias for [`Identifier::new`], provided for convenience.
pub fn new_valid<S: AsRef<str>>(string: S) -> Result<Self, ParseError> {
IdentifierParser::new()
.parse(string.as_ref())
.map(Identifier::new) // Use the internal new method for valid identifiers
.map_err(|e| ParseError::IdentifierParseError {
error: format!(
"Failed to parse identifier: {}. (reserved keyword or invalid format)",
e
),
})
}

/// The name of this [Identifier]
/// It already implements [Deref] to [str], so this method is not necessary for most use cases.
#[must_use]
Expand Down
9 changes: 4 additions & 5 deletions crates/proof-of-sql-parser/src/intermediate_ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
* https://docs.rs/vervolg/latest/vervolg/ast/enum.Statement.html
***/

use crate::{posql_time::PoSQLTimestamp, Identifier};
use crate::{intermediate_decimal::IntermediateDecimal, posql_time::PoSQLTimestamp, Identifier};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have IntermediateDecimal any more which should make things easier.

use alloc::{boxed::Box, string::String, vec::Vec};
use bigdecimal::BigDecimal;
use core::{
fmt,
fmt::{Display, Formatter},
Expand Down Expand Up @@ -346,7 +345,7 @@ pub enum Literal {
/// String Literal
VarChar(String),
/// Decimal Literal
Decimal(BigDecimal),
Decimal(IntermediateDecimal),
/// Timestamp Literal
Timestamp(PoSQLTimestamp),
}
Expand Down Expand Up @@ -396,8 +395,8 @@ macro_rules! impl_string_to_literal {
impl_string_to_literal!(&str);
impl_string_to_literal!(String);

impl From<BigDecimal> for Literal {
fn from(val: BigDecimal) -> Self {
impl From<IntermediateDecimal> for Literal {
fn from(val: IntermediateDecimal) -> Self {
Literal::Decimal(val)
}
}
Expand Down
17 changes: 13 additions & 4 deletions crates/proof-of-sql-parser/src/intermediate_ast_tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::{
intermediate_ast::OrderByDirection::{Asc, Desc},
intermediate_decimal::IntermediateDecimal,
sql::*,
utility::*,
SelectStatement,
Expand All @@ -9,7 +10,6 @@ use alloc::{
string::{String, ToString},
vec,
};
use bigdecimal::BigDecimal;

// Sting parser tests
#[test]
Expand Down Expand Up @@ -143,7 +143,10 @@ fn we_can_parse_a_query_with_constants() {
col_res(lit(3), "bigint"),
col_res(lit(true), "boolean"),
col_res(lit("proof"), "varchar"),
col_res(lit("-2.34".parse::<BigDecimal>().unwrap()), "decimal"),
col_res(
lit(IntermediateDecimal::try_from("-2.34").unwrap()),
"decimal",
),
],
tab(None, "sxt_tab"),
vec![],
Expand Down Expand Up @@ -217,7 +220,10 @@ fn we_can_parse_a_query_with_a_column_equals_a_decimal() {
query(
cols_res(&["a"]),
tab(None, "sxt_tab"),
equal(col("a"), lit("-0.32".parse::<BigDecimal>().unwrap())),
equal(
col("a"),
lit(IntermediateDecimal::try_from("-0.32").unwrap()),
),
vec![],
),
vec![],
Expand Down Expand Up @@ -435,7 +441,10 @@ fn we_can_parse_a_query_with_one_logical_or_filter_expression() {
tab(None, "sxt_tab"),
or(
equal(col("b"), lit(3)),
equal(col("c"), lit("-2.34".parse::<BigDecimal>().unwrap())),
equal(
col("c"),
lit(IntermediateDecimal::try_from("-2.34").unwrap()),
),
),
vec![],
),
Expand Down
273 changes: 273 additions & 0 deletions crates/proof-of-sql-parser/src/intermediate_decimal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
//! A parser conforming to standard postgreSQL to parse the precision and scale
//! from a decimal token obtained from the lalrpop lexer. This module
//! exists to resolve a cyclic dependency between proof-of-sql
//! and proof-of-sql-parser.
//!
//! A decimal must have a decimal point. The lexer does not route
//! whole integers to this contructor.
use crate::intermediate_decimal::IntermediateDecimalError::{LossyCast, OutOfRange, ParseError};
use alloc::string::String;
use bigdecimal::{num_bigint::BigInt, BigDecimal, ParseBigDecimalError, ToPrimitive};
use core::{fmt, hash::Hash, str::FromStr};
use serde::{Deserialize, Serialize};
use snafu::Snafu;

/// Errors related to the processing of decimal values in proof-of-sql
#[allow(clippy::module_name_repetitions)]
#[derive(Snafu, Debug, PartialEq)]
pub enum IntermediateDecimalError {
/// Represents an error encountered during the parsing of a decimal string.
#[snafu(display("{error}"))]
ParseError {
/// The underlying error
error: ParseBigDecimalError,
},
/// Error occurs when this decimal cannot fit in a primitive.
#[snafu(display("Value out of range for target type"))]
OutOfRange,
/// Error occurs when this decimal cannot be losslessly cast into a primitive.
#[snafu(display("Fractional part of decimal is non-zero"))]
LossyCast,
/// Cannot cast this decimal to a big integer
#[snafu(display("Conversion to integer failed"))]
ConversionFailure,
}
impl From<ParseBigDecimalError> for IntermediateDecimalError {
fn from(value: ParseBigDecimalError) -> Self {
IntermediateDecimalError::ParseError { error: value }
}
}

impl Eq for IntermediateDecimalError {}

/// An intermediate placeholder for a decimal
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Hash)]
pub struct IntermediateDecimal {
value: BigDecimal,
}

impl IntermediateDecimal {
/// Get the integer part of the fixed-point representation of this intermediate decimal.
#[must_use]
pub fn value(&self) -> BigDecimal {
self.value.clone()
}

/// Get the precision of the fixed-point representation of this intermediate decimal.
#[must_use]
pub fn precision(&self) -> u8 {
match u8::try_from(self.value.digits()) {
Ok(v) => v,
Err(_) => u8::MAX, // Returning u8::MAX on truncation
}
}

/// Get the scale of the fixed-point representation of this intermediate decimal.
#[must_use]
pub fn scale(&self) -> i8 {
match i8::try_from(self.value.fractional_digit_count()) {
Ok(v) => v,
Err(_) => i8::MAX, // Returning i8::MAX on truncation
}
}

/// Attempts to convert the decimal to `BigInt` while adjusting it to the specified precision and scale.
/// Returns an error if the conversion cannot be performed due to precision or scale constraints.
///
/// # Errors
/// Returns an `IntermediateDecimalError::LossyCast` error if the number of digits in the scaled decimal exceeds the specified precision.
pub fn try_into_bigint_with_precision_and_scale(
&self,
precision: u8,
scale: i8,
) -> Result<BigInt, IntermediateDecimalError> {
let scaled_decimal = self.value.with_scale(scale.into());
if scaled_decimal.digits() > precision.into() {
return Err(LossyCast);
}
let (d, _) = scaled_decimal.into_bigint_and_exponent();
Ok(d)
}
}

impl fmt::Display for IntermediateDecimal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.value)
}
}

impl FromStr for IntermediateDecimal {
type Err = IntermediateDecimalError;

fn from_str(decimal_string: &str) -> Result<Self, Self::Err> {
BigDecimal::from_str(decimal_string)
.map(|value| IntermediateDecimal {
value: value.normalized(),
})
.map_err(|err| ParseError { error: err })
}
}

impl From<i128> for IntermediateDecimal {
fn from(value: i128) -> Self {
IntermediateDecimal {
value: BigDecimal::from(value),
}
}
}

impl From<i64> for IntermediateDecimal {
fn from(value: i64) -> Self {
IntermediateDecimal {
value: BigDecimal::from(value),
}
}
}

impl TryFrom<&str> for IntermediateDecimal {
type Error = IntermediateDecimalError;

fn try_from(s: &str) -> Result<Self, Self::Error> {
IntermediateDecimal::from_str(s)
}
}

impl TryFrom<String> for IntermediateDecimal {
type Error = IntermediateDecimalError;

fn try_from(s: String) -> Result<Self, Self::Error> {
IntermediateDecimal::from_str(&s)
}
}

impl TryFrom<IntermediateDecimal> for i128 {
type Error = IntermediateDecimalError;

fn try_from(decimal: IntermediateDecimal) -> Result<Self, Self::Error> {
if !decimal.value.is_integer() {
return Err(LossyCast);
}

match decimal.value.to_i128() {
Some(value) if (i128::MIN..=i128::MAX).contains(&value) => Ok(value),
_ => Err(OutOfRange),
}
}
}

impl TryFrom<IntermediateDecimal> for i64 {
type Error = IntermediateDecimalError;

fn try_from(decimal: IntermediateDecimal) -> Result<Self, Self::Error> {
if !decimal.value.is_integer() {
return Err(LossyCast);
}

match decimal.value.to_i64() {
Some(value) if (i64::MIN..=i64::MAX).contains(&value) => Ok(value),
_ => Err(OutOfRange),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;

#[test]
fn test_valid_decimal_simple() {
let decimal = "123.45".parse();
assert!(decimal.is_ok());
let unwrapped_decimal: IntermediateDecimal = decimal.unwrap();
assert_eq!(unwrapped_decimal.to_string(), "123.45");
assert_eq!(unwrapped_decimal.precision(), 5);
assert_eq!(unwrapped_decimal.scale(), 2);
}

#[test]
fn test_valid_decimal_with_leading_and_trailing_zeros() {
let decimal = "000123.45000".parse();
assert!(decimal.is_ok());
let unwrapped_decimal: IntermediateDecimal = decimal.unwrap();
assert_eq!(unwrapped_decimal.to_string(), "123.45");
assert_eq!(unwrapped_decimal.precision(), 5);
assert_eq!(unwrapped_decimal.scale(), 2);
}

#[test]
fn test_accessors() {
let decimal: IntermediateDecimal = "123.456".parse().unwrap();
assert_eq!(decimal.to_string(), "123.456");
assert_eq!(decimal.precision(), 6);
assert_eq!(decimal.scale(), 3);
}

#[test]
fn test_conversion_to_i128() {
let valid_decimal = IntermediateDecimal {
value: BigDecimal::from_str("170141183460469231731687303715884105727").unwrap(),
};
assert_eq!(
i128::try_from(valid_decimal),
Ok(170_141_183_460_469_231_731_687_303_715_884_105_727_i128)
);

let valid_decimal = IntermediateDecimal {
value: BigDecimal::from_str("123.000").unwrap(),
};
assert_eq!(i128::try_from(valid_decimal), Ok(123));

let overflow_decimal = IntermediateDecimal {
value: BigDecimal::from_str("170141183460469231731687303715884105728").unwrap(),
};
assert_eq!(i128::try_from(overflow_decimal), Err(OutOfRange));

let valid_decimal_negative = IntermediateDecimal {
value: BigDecimal::from_str("-170141183460469231731687303715884105728").unwrap(),
};
assert_eq!(
i128::try_from(valid_decimal_negative),
Ok(-170_141_183_460_469_231_731_687_303_715_884_105_728_i128)
);

let non_integer = IntermediateDecimal {
value: BigDecimal::from_str("100.5").unwrap(),
};
assert_eq!(i128::try_from(non_integer), Err(LossyCast));
}

#[test]
fn test_conversion_to_i64() {
let valid_decimal = IntermediateDecimal {
value: BigDecimal::from_str("9223372036854775807").unwrap(),
};
assert_eq!(
i64::try_from(valid_decimal),
Ok(9_223_372_036_854_775_807_i64)
);

let valid_decimal = IntermediateDecimal {
value: BigDecimal::from_str("123.000").unwrap(),
};
assert_eq!(i64::try_from(valid_decimal), Ok(123));

let overflow_decimal = IntermediateDecimal {
value: BigDecimal::from_str("9223372036854775808").unwrap(),
};
assert_eq!(i64::try_from(overflow_decimal), Err(OutOfRange));

let valid_decimal_negative = IntermediateDecimal {
value: BigDecimal::from_str("-9223372036854775808").unwrap(),
};
assert_eq!(
i64::try_from(valid_decimal_negative),
Ok(-9_223_372_036_854_775_808_i64)
);

let non_integer = IntermediateDecimal {
value: BigDecimal::from_str("100.5").unwrap(),
};
assert_eq!(i64::try_from(non_integer), Err(LossyCast));
}
}
2 changes: 2 additions & 0 deletions crates/proof-of-sql-parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#![cfg_attr(test, allow(clippy::missing_panics_doc))]
extern crate alloc;

/// Module for handling an intermediate decimal type received from the lexer.
pub mod intermediate_decimal;
/// Module for handling an intermediate timestamp type received from the lexer.
pub mod posql_time;
#[macro_use]
Expand Down
Loading
Loading