add support for any() and all() array comparisons
This commit is contained in:
parent
928f6cb759
commit
b2257ab1c0
6 changed files with 258 additions and 89 deletions
52
phono-pestgros/src/array_tests.rs
Normal file
52
phono-pestgros/src/array_tests.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
use crate::{Datum, Expr, InfixOp};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_array_parses() {
|
||||||
|
assert_eq!(Expr::try_from("array[]"), Ok(Expr::Array(vec![])));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn array_of_literals_parses() {
|
||||||
|
assert_eq!(
|
||||||
|
Expr::try_from("array[1, 2, 3]"),
|
||||||
|
Ok(Expr::Array(vec![
|
||||||
|
Expr::Literal(Datum::Numeric(Some(1.into()))),
|
||||||
|
Expr::Literal(Datum::Numeric(Some(2.into()))),
|
||||||
|
Expr::Literal(Datum::Numeric(Some(3.into()))),
|
||||||
|
])),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn array_of_exprs_parses() {
|
||||||
|
assert_eq!(
|
||||||
|
Expr::try_from("array[(1), 2 + 3]"),
|
||||||
|
Ok(Expr::Array(vec![
|
||||||
|
Expr::Literal(Datum::Numeric(Some(1.into()))),
|
||||||
|
Expr::Infix {
|
||||||
|
lhs: Box::new(Expr::Literal(Datum::Numeric(Some(2.into())))),
|
||||||
|
op: InfixOp::Add,
|
||||||
|
rhs: Box::new(Expr::Literal(Datum::Numeric(Some(3.into())))),
|
||||||
|
},
|
||||||
|
])),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn array_cmp_modifier_parses() {
|
||||||
|
assert_eq!(
|
||||||
|
Expr::try_from("3 = any(array[3])"),
|
||||||
|
Ok(Expr::Infix {
|
||||||
|
lhs: Box::new(Expr::Literal(Datum::Numeric(Some(3.into())))),
|
||||||
|
op: InfixOp::WithCmpModifierAny(Box::new(InfixOp::Eq)),
|
||||||
|
rhs: Box::new(Expr::Array(vec![Expr::Literal(Datum::Numeric(Some(
|
||||||
|
3.into()
|
||||||
|
)))]))
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_parenthesized_array_cmp_modifier_fails() {
|
||||||
|
assert!(Expr::try_from("3 = any array[3]").is_err());
|
||||||
|
}
|
||||||
|
|
@ -13,3 +13,13 @@ fn sql_converts_to_query_builder() -> Result<(), Box<dyn Error>> {
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmp_array_modifier_round_trips() -> Result<(), Box<dyn Error>> {
|
||||||
|
let expr = Expr::try_from("1 = 2 and 3 < any(array[4])")?;
|
||||||
|
assert_eq!(
|
||||||
|
QueryBuilder::<'_, Postgres>::from(expr).sql(),
|
||||||
|
"(($1) = ($2)) and (($3) < any (array[($4)]))",
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,14 @@
|
||||||
//! https://github.com/pest-parser/pest/blob/master/grammars/src/grammars/sql.pest.
|
//! https://github.com/pest-parser/pest/blob/master/grammars/src/grammars/sql.pest.
|
||||||
//! (Original is dual-licensed under MIT/Apache-2.0.)
|
//! (Original is dual-licensed under MIT/Apache-2.0.)
|
||||||
//!
|
//!
|
||||||
//! Postgres largely conforms to the SQLite flavored dialect captured by the
|
//! PostgreSQL departs extensively from the SQLite flavored dialect captured in
|
||||||
//! original grammar, but its rules for identifiers differ:
|
//! the original grammar. For example, rules for identifiers/object names
|
||||||
|
//! differ, as do keywords, built-in types, and syntax for specifying function
|
||||||
|
//! arguments, type modifiers, CTEs, and so on.
|
||||||
//!
|
//!
|
||||||
//! > SQL identifiers and key words must begin with a letter (a-z, but also
|
//! This grammar covers a larger subset of the Postgres SQL dialect, but it is a
|
||||||
//! > letters with diacritical marks and non-Latin letters) or an underscore
|
//! work in progress and is far from complete. It should only be used to parse
|
||||||
//! > (_). Subsequent characters in an identifier or key word can be letters,
|
//! input that is "PostgreSQL-esque", not input that expects spec compliance.
|
||||||
//! > underscores, digits (0-9), or dollar signs ($). Note that dollar signs are
|
|
||||||
//! > not allowed in identifiers according to the letter of the SQL standard,
|
|
||||||
//! > so their use might render applications less portable.
|
|
||||||
//! -- https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
|
|
||||||
|
|
||||||
Command = _{ SOI ~ (Query | ExplainQuery | DDL | ACL) ~ EOF }
|
Command = _{ SOI ~ (Query | ExplainQuery | DDL | ACL) ~ EOF }
|
||||||
|
|
||||||
|
|
@ -65,7 +63,7 @@ DDL = _{ CreateTable | DropTable | CreateProc }
|
||||||
Distribution
|
Distribution
|
||||||
}
|
}
|
||||||
Columns = { ColumnDef ~ ("," ~ ColumnDef)* }
|
Columns = { ColumnDef ~ ("," ~ ColumnDef)* }
|
||||||
ColumnDef = { Identifier ~ ColumnDefType ~ ColumnDefIsNull? }
|
ColumnDef = { Identifier ~ TypeCast ~ ColumnDefIsNull? }
|
||||||
ColumnDefIsNull = { NotFlag? ~ ^"null" }
|
ColumnDefIsNull = { NotFlag? ~ ^"null" }
|
||||||
PrimaryKey = {
|
PrimaryKey = {
|
||||||
^"primary" ~ ^"key" ~
|
^"primary" ~ ^"key" ~
|
||||||
|
|
@ -82,7 +80,7 @@ DDL = _{ CreateTable | DropTable | CreateProc }
|
||||||
((^"as" ~ "$$" ~ ProcBody ~ "$$") | (^"begin" ~ "atomic" ~ ProcBody ~ "end"))
|
((^"as" ~ "$$" ~ ProcBody ~ "$$") | (^"begin" ~ "atomic" ~ ProcBody ~ "end"))
|
||||||
}
|
}
|
||||||
ProcParams = { ProcParamDef ~ ("," ~ ProcParamDef)* }
|
ProcParams = { ProcParamDef ~ ("," ~ ProcParamDef)* }
|
||||||
ProcParamDef = { ColumnDefType }
|
ProcParamDef = { TypeCast }
|
||||||
ProcLanguage = { SQL }
|
ProcLanguage = { SQL }
|
||||||
SQL = { ^"sql" }
|
SQL = { ^"sql" }
|
||||||
ProcBody = { (Insert | Update | Delete) }
|
ProcBody = { (Insert | Update | Delete) }
|
||||||
|
|
@ -130,38 +128,48 @@ Query = { (SelectWithOptionalContinuation | Values | Insert | Update | Delete) }
|
||||||
|
|
||||||
Identifier = ${ DoubleQuotedIdentifier | UnquotedIdentifier }
|
Identifier = ${ DoubleQuotedIdentifier | UnquotedIdentifier }
|
||||||
DoubleQuotedIdentifier = @{ "\"" ~ ("\"\"" | '\u{01}'..'\u{21}' | '\u{23}'..'\u{10FFFF}')+ ~ "\"" }
|
DoubleQuotedIdentifier = @{ "\"" ~ ("\"\"" | '\u{01}'..'\u{21}' | '\u{23}'..'\u{10FFFF}')+ ~ "\"" }
|
||||||
UnquotedIdentifier = @{ !(Keyword ~ ("(" | WHITESPACE | "," | EOF)) ~ (UnquotedIdentifierStart ~ UnquotedIdentifierRemainder*) }
|
UnquotedIdentifier = @{ !(Keyword ~ ("(" | "[" | WHITESPACE | "," | EOF)) ~ (UnquotedIdentifierStart ~ UnquotedIdentifierRemainder*) }
|
||||||
UnquotedIdentifierStart = _{ 'a'..'я' | 'A'..'Я' | "_" }
|
UnquotedIdentifierStart = _{ 'a'..'я' | 'A'..'Я' | "_" }
|
||||||
UnquotedIdentifierRemainder = _{ UnquotedIdentifierStart | "$" | ASCII_DIGIT }
|
UnquotedIdentifierRemainder = _{ UnquotedIdentifierStart | "$" | ASCII_DIGIT }
|
||||||
Keyword = { ^"left" | ^"having" | ^"not" | ^"inner" | ^"group"
|
Keyword = { ^"left" | ^"having" | ^"not" | ^"inner" | ^"group"
|
||||||
| ^"on" | ^"join" | ^"from" | ^"exists" | ^"except"
|
| ^"on" | ^"join" | ^"from" | ^"exists" | ^"except"
|
||||||
| ^"union" | ^"where" | ^"distinct" | ^"between" | ^"option"
|
| ^"union" | ^"where" | ^"distinct" | ^"between" | ^"option"
|
||||||
| ^"values"}
|
| ^"values" | ^"with" | ^"as" | ^"array" | ^"any" | ^"some"
|
||||||
|
| ^"all" | ^"in" }
|
||||||
|
|
||||||
ExprRoot = _{ &SOI ~ Expr ~ &EOI }
|
ExprRoot = _{ &SOI ~ Expr ~ &EOI }
|
||||||
Expr = { ExprAtomValue ~ (ExprInfixOp ~ ExprAtomValue)* }
|
Expr = { ExprAtomValue ~ (ExprInfixOp ~ ExprAtomValue)* }
|
||||||
ExprInfixOp = _{ Between | ArithInfixOp | CmpInfixOp | ConcatInfixOp | And | Or }
|
ExprInfixOp = _{ Between | NonCmpInfixOp | CmpInfixOp | ConcatInfixOp | And | Or }
|
||||||
Between = { NotFlag? ~ ^"between" }
|
Between = { NotFlag? ~ ^"between" }
|
||||||
And = { ^"and" }
|
And = { ^"and" }
|
||||||
Or = { ^"or" }
|
Or = { ^"or" }
|
||||||
ConcatInfixOp = { "||" }
|
ConcatInfixOp = { "||" }
|
||||||
ArithInfixOp = _{ Add | Subtract | Multiply | Divide }
|
CmpInfixOp = { (NotEq | GtEq | Gt | LtEq | Lt | Eq | Lt) ~ (CmpArrayModifier ~ &ExpressionInParentheses)? }
|
||||||
Add = { "+" }
|
|
||||||
Subtract = { "-" }
|
|
||||||
Multiply = { "*" }
|
|
||||||
Divide = { "/" }
|
|
||||||
CmpInfixOp = _{ NotEq | GtEq | Gt | LtEq | Lt | Eq | Lt | In }
|
|
||||||
Eq = { "=" }
|
Eq = { "=" }
|
||||||
Gt = { ">" }
|
Gt = { ">" }
|
||||||
GtEq = { ">=" }
|
GtEq = { ">=" }
|
||||||
Lt = { "<" }
|
Lt = { "<" }
|
||||||
LtEq = { "<=" }
|
LtEq = { "<=" }
|
||||||
NotEq = { "<>" | "!=" }
|
NotEq = { "<>" | "!=" }
|
||||||
|
NonCmpInfixOp = _{ Add | Subtract | Multiply | Divide | In }
|
||||||
|
Add = { "+" }
|
||||||
|
Subtract = { "-" }
|
||||||
|
Multiply = { "*" }
|
||||||
|
Divide = { "/" }
|
||||||
In = { NotFlag? ~ ^"in" }
|
In = { NotFlag? ~ ^"in" }
|
||||||
|
CmpArrayModifier = { CmpModifierAny | CmpModifierAll }
|
||||||
|
CmpModifierAny = { ^"any" | ^"some "}
|
||||||
|
CmpModifierAll = { ^"all" }
|
||||||
ExprAtomValue = _{ UnaryNot* ~ AtomicExpr ~ IsNullPostfix? }
|
ExprAtomValue = _{ UnaryNot* ~ AtomicExpr ~ IsNullPostfix? }
|
||||||
UnaryNot = @{ NotFlag }
|
UnaryNot = @{ NotFlag }
|
||||||
IsNullPostfix = { ^"is" ~ NotFlag? ~ ^"null" }
|
IsNullPostfix = { ^"is" ~ NotFlag? ~ ^"null" }
|
||||||
AtomicExpr = _{ Literal | Parameter | Cast | IdentifierWithOptionalContinuation | ExpressionInParentheses | UnaryOperator | SubQuery | Row }
|
AtomicExpr = _{ Literal | Parameter | IdentifierWithOptionalContinuation | ExpressionInParentheses | UnaryOperator | SubQuery | Row | SquareBracketArray }
|
||||||
|
// TODO: Empty arrays don't parse without the `!"]"` prefix in the
|
||||||
|
// optional sequence of sub-expressions, but the reason is not
|
||||||
|
// immediately clear: the ']' character doesn't seem like it should
|
||||||
|
// be compatible with the beginning of any `AtomicExpr`. This may
|
||||||
|
// be worth investigating.
|
||||||
|
SquareBracketArray = { ^"array" ~ "[" ~ (!"]" ~ (Expr ~ ("," ~ Expr)*))? ~ "]" }
|
||||||
Literal = _{ True | False | Null | Double | Decimal | Unsigned | Integer | SingleQuotedString }
|
Literal = _{ True | False | Null | Double | Decimal | Unsigned | Integer | SingleQuotedString }
|
||||||
True = { ^"true" }
|
True = { ^"true" }
|
||||||
False = { ^"false" }
|
False = { ^"false" }
|
||||||
|
|
@ -184,24 +192,34 @@ Expr = { ExprAtomValue ~ (ExprInfixOp ~ ExprAtomValue)* }
|
||||||
FunctionInvocationContinuation = { "(" ~ (CountAsterisk | FunctionArgs)? ~ ")" }
|
FunctionInvocationContinuation = { "(" ~ (CountAsterisk | FunctionArgs)? ~ ")" }
|
||||||
// TODO: Support named argument notation
|
// TODO: Support named argument notation
|
||||||
// (`my_func(name => value)`).
|
// (`my_func(name => value)`).
|
||||||
|
// TODO: Support keywords within args list as applicable.
|
||||||
FunctionArgs = { Distinct? ~ (Expr ~ ("," ~ Expr)*)? }
|
FunctionArgs = { Distinct? ~ (Expr ~ ("," ~ Expr)*)? }
|
||||||
CountAsterisk = { "*" }
|
CountAsterisk = { "*" }
|
||||||
ExpressionInParentheses = { "(" ~ Expr ~ ")" }
|
ExpressionInParentheses = { "(" ~ Expr ~ ")" }
|
||||||
Cast = { ^"cast" ~ "(" ~ Expr ~ ^"as" ~ TypeCast ~ ")" }
|
CastInfix = { Expr ~ "::" ~ TypeCast }
|
||||||
TypeCast = _{ TypeAny | ColumnDefType }
|
TypeCast = {
|
||||||
ColumnDefType = { TypeBool | TypeDecimal | TypeDouble | TypeInt | TypeNumber
|
TypeBool
|
||||||
| TypeScalar | TypeString | TypeText | TypeUnsigned | TypeVarchar }
|
| TypeDecimal
|
||||||
TypeAny = { ^"any" }
|
| TypeDouble
|
||||||
|
| TypeInt
|
||||||
|
| TypeNumeric
|
||||||
|
| TypeText
|
||||||
|
| TypeVarchar
|
||||||
|
}
|
||||||
TypeBool = { (^"boolean" | ^"bool") }
|
TypeBool = { (^"boolean" | ^"bool") }
|
||||||
TypeDecimal = { ^"decimal" }
|
TypeDecimal = { ^"decimal" }
|
||||||
TypeDouble = { ^"double" }
|
TypeDouble = { ^"double" }
|
||||||
TypeInt = { (^"integer" | ^"int") }
|
TypeInt = { (^"integer" | ^"int") }
|
||||||
TypeNumber = { ^"number" }
|
TypeNumeric = { ^"numeric" }
|
||||||
TypeScalar = { ^"scalar" }
|
|
||||||
TypeString = { ^"string" }
|
|
||||||
TypeText = { ^"text" }
|
TypeText = { ^"text" }
|
||||||
TypeUnsigned = { ^"unsigned" }
|
|
||||||
TypeVarchar = { ^"varchar" ~ "(" ~ Unsigned ~ ")" }
|
TypeVarchar = { ^"varchar" ~ "(" ~ Unsigned ~ ")" }
|
||||||
|
|
||||||
|
TypeDate = { ^"date" }
|
||||||
|
TypeTime = { ^"time" ~ Unsigned? ~ (WithTimeZone | WithoutTimeZone)? }
|
||||||
|
TypeTimestamp = { ^"timestamp" ~ Unsigned? ~ (WithTimeZone | WithoutTimeZone)? }
|
||||||
|
WithTimeZone = { ^"with" ~ ^"time" ~ ^"zone" }
|
||||||
|
WithoutTimeZone = { ^"without" ~ ^"time" ~ ^"zone" }
|
||||||
|
|
||||||
UnaryOperator = _{ Exists }
|
UnaryOperator = _{ Exists }
|
||||||
Exists = { NotFlag? ~ ^"exists" ~ SubQuery }
|
Exists = { NotFlag? ~ ^"exists" ~ SubQuery }
|
||||||
Row = { "(" ~ Expr ~ ("," ~ Expr)* ~ ")" }
|
Row = { "(" ~ Expr ~ ("," ~ Expr)* ~ ")" }
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
//! expressions and more, based on a modified version of the
|
//! expressions and more, based on a modified version of the
|
||||||
//! [official Pest SQL grammar](https://github.com/pest-parser/pest/blob/79dd30d11aab6f0fba3cd79bd48f456209b966b3/grammars/src/grammars/sql.pest).
|
//! [official Pest SQL grammar](https://github.com/pest-parser/pest/blob/79dd30d11aab6f0fba3cd79bd48f456209b966b3/grammars/src/grammars/sql.pest).
|
||||||
//!
|
//!
|
||||||
|
//! This grammar covers a larger subset of the Postgres SQL dialect, but it is a
|
||||||
|
//! work in progress and is far from complete. It should only be used to parse
|
||||||
|
//! input that is "PostgreSQL-esque", not input that expects spec compliance.
|
||||||
|
//!
|
||||||
//! ## Example
|
//! ## Example
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
|
|
@ -40,6 +44,8 @@ pub use crate::{datum::Datum, query_builders::QueryFragment};
|
||||||
mod datum;
|
mod datum;
|
||||||
mod query_builders;
|
mod query_builders;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod array_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod fragment_tests;
|
mod fragment_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -136,18 +142,13 @@ static PRATT_PARSER: LazyLock<PrattParser<Rule>> = LazyLock::new(|| {
|
||||||
.op(Op::infix(Rule::Between, Left))
|
.op(Op::infix(Rule::Between, Left))
|
||||||
.op(Op::infix(Rule::And, Left))
|
.op(Op::infix(Rule::And, Left))
|
||||||
.op(Op::prefix(Rule::UnaryNot))
|
.op(Op::prefix(Rule::UnaryNot))
|
||||||
.op(Op::infix(Rule::Eq, Right)
|
.op(Op::infix(Rule::CmpInfixOp, Right))
|
||||||
| Op::infix(Rule::NotEq, Right)
|
|
||||||
| Op::infix(Rule::Gt, Right)
|
|
||||||
| Op::infix(Rule::GtEq, Right)
|
|
||||||
| Op::infix(Rule::Lt, Right)
|
|
||||||
| Op::infix(Rule::LtEq, Right)
|
|
||||||
| Op::infix(Rule::In, Right))
|
|
||||||
// Official Pest example overstates the concat operator's precedence. It
|
// Official Pest example overstates the concat operator's precedence. It
|
||||||
// should be lower precedence than add/subtract.
|
// should be lower precedence than add/subtract.
|
||||||
.op(Op::infix(Rule::ConcatInfixOp, Left))
|
.op(Op::infix(Rule::ConcatInfixOp, Left))
|
||||||
.op(Op::infix(Rule::Add, Left) | Op::infix(Rule::Subtract, Left))
|
.op(Op::infix(Rule::Add, Left) | Op::infix(Rule::Subtract, Left))
|
||||||
.op(Op::infix(Rule::Multiply, Left) | Op::infix(Rule::Divide, Left))
|
.op(Op::infix(Rule::Multiply, Left) | Op::infix(Rule::Divide, Left))
|
||||||
|
.op(Op::infix(Rule::CastInfix, Left))
|
||||||
.op(Op::postfix(Rule::IsNullPostfix))
|
.op(Op::postfix(Rule::IsNullPostfix))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -174,6 +175,7 @@ pub enum Expr {
|
||||||
is_null: bool,
|
is_null: bool,
|
||||||
expr: Box<Expr>,
|
expr: Box<Expr>,
|
||||||
},
|
},
|
||||||
|
Array(Vec<Expr>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for Expr {
|
impl TryFrom<&str> for Expr {
|
||||||
|
|
@ -188,7 +190,7 @@ impl TryFrom<&str> for Expr {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum InfixOp {
|
pub enum InfixOp {
|
||||||
// Arithmetic ops:
|
// Arithmetic ops:
|
||||||
Add,
|
Add,
|
||||||
|
|
@ -206,6 +208,18 @@ pub enum InfixOp {
|
||||||
Lt,
|
Lt,
|
||||||
Lte,
|
Lte,
|
||||||
Neq,
|
Neq,
|
||||||
|
|
||||||
|
// Miscellaneous ops:
|
||||||
|
Cast,
|
||||||
|
|
||||||
|
// Array comparison modifiers (such as `= any(array[])`):
|
||||||
|
// TODO: This is an awkward pattern, which is capable of representing
|
||||||
|
// invalid expressions (such as `3 + any(array[])`). I expect it'll need to
|
||||||
|
// be rewritten at some point anyways to handle other keyword-driven infix
|
||||||
|
// syntax, but for expediency I'm leaving a more robust solution as a
|
||||||
|
// challenge for another day.
|
||||||
|
WithCmpModifierAny(Box<Self>),
|
||||||
|
WithCmpModifierAll(Box<Self>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
|
@ -267,6 +281,18 @@ fn parse_expr_pairs(expr_pairs: Pairs<'_, Rule>) -> Result<Expr, ParseError> {
|
||||||
Expr::ObjName(name)
|
Expr::ObjName(name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Rule::SquareBracketArray => {
|
||||||
|
let mut arr_items: Vec<Expr> = vec![];
|
||||||
|
for inner_pair in pair.into_inner() {
|
||||||
|
match inner_pair.as_rule() {
|
||||||
|
Rule::Expr => {arr_items.push(parse_expr_pairs(inner_pair.into_inner())?);}
|
||||||
|
_ => unreachable!(
|
||||||
|
"SquareBracketArray has only Exprs as direct child rules",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Expr::Array(arr_items))
|
||||||
|
}
|
||||||
rule => Err(ParseError::UnknownRule(rule)),
|
rule => Err(ParseError::UnknownRule(rule)),
|
||||||
})
|
})
|
||||||
.map_infix(|lhs, op, rhs| Ok(Expr::Infix {
|
.map_infix(|lhs, op, rhs| Ok(Expr::Infix {
|
||||||
|
|
@ -278,13 +304,9 @@ fn parse_expr_pairs(expr_pairs: Pairs<'_, Rule>) -> Result<Expr, ParseError> {
|
||||||
Rule::Multiply => InfixOp::Mult,
|
Rule::Multiply => InfixOp::Mult,
|
||||||
Rule::Subtract => InfixOp::Sub,
|
Rule::Subtract => InfixOp::Sub,
|
||||||
Rule::And => InfixOp::And,
|
Rule::And => InfixOp::And,
|
||||||
Rule::Eq => InfixOp::Eq,
|
Rule::CmpInfixOp => parse_cmp_op(op)?,
|
||||||
Rule::Gt => InfixOp::Gt,
|
|
||||||
Rule::GtEq => InfixOp::Gte,
|
|
||||||
Rule::Lt => InfixOp::Lt,
|
|
||||||
Rule::LtEq => InfixOp::Lte,
|
|
||||||
Rule::NotEq => InfixOp::Neq,
|
|
||||||
Rule::Or => InfixOp::Or,
|
Rule::Or => InfixOp::Or,
|
||||||
|
Rule::CastInfix => InfixOp::Cast,
|
||||||
rule => Err(ParseError::UnknownRule(rule))?,
|
rule => Err(ParseError::UnknownRule(rule))?,
|
||||||
},
|
},
|
||||||
rhs: Box::new(rhs?),
|
rhs: Box::new(rhs?),
|
||||||
|
|
@ -306,6 +328,52 @@ fn parse_expr_pairs(expr_pairs: Pairs<'_, Rule>) -> Result<Expr, ParseError> {
|
||||||
.parse(expr_pairs)
|
.parse(expr_pairs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_cmp_op(op: Pair<'_, Rule>) -> Result<InfixOp, ParseError> {
|
||||||
|
let mut base_op: Option<InfixOp> = None;
|
||||||
|
for inner in op.into_inner() {
|
||||||
|
match inner.as_rule() {
|
||||||
|
Rule::Eq => {
|
||||||
|
base_op = Some(InfixOp::Eq);
|
||||||
|
}
|
||||||
|
Rule::Gt => {
|
||||||
|
base_op = Some(InfixOp::Gt);
|
||||||
|
}
|
||||||
|
Rule::GtEq => {
|
||||||
|
base_op = Some(InfixOp::Gte);
|
||||||
|
}
|
||||||
|
Rule::Lt => {
|
||||||
|
base_op = Some(InfixOp::Lt);
|
||||||
|
}
|
||||||
|
Rule::LtEq => {
|
||||||
|
base_op = Some(InfixOp::Lte);
|
||||||
|
}
|
||||||
|
Rule::NotEq => {
|
||||||
|
base_op = Some(InfixOp::Neq);
|
||||||
|
}
|
||||||
|
Rule::CmpArrayModifier => {
|
||||||
|
if let Some(base_op) = base_op {
|
||||||
|
return Ok(
|
||||||
|
match inner
|
||||||
|
.into_inner()
|
||||||
|
.next()
|
||||||
|
.expect("CmpArrayModifier should be a simple enumeration")
|
||||||
|
.as_rule()
|
||||||
|
{
|
||||||
|
Rule::CmpModifierAny => InfixOp::WithCmpModifierAny(Box::new(base_op)),
|
||||||
|
Rule::CmpModifierAll => InfixOp::WithCmpModifierAll(Box::new(base_op)),
|
||||||
|
rule => Err(ParseError::UnknownRule(rule))?,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Err(ParseError::UnknownRule(Rule::CmpArrayModifier));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rule => Err(ParseError::UnknownRule(rule))?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(base_op.expect("CmpInfixOp always has at least one child"))
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_function_invocation_continuation(pair: Pair<'_, Rule>) -> Result<FnArgs, ParseError> {
|
fn parse_function_invocation_continuation(pair: Pair<'_, Rule>) -> Result<FnArgs, ParseError> {
|
||||||
let mut cont_inner_iter = pair.into_inner();
|
let mut cont_inner_iter = pair.into_inner();
|
||||||
let fn_args = if let Some(cont_inner) = cont_inner_iter.next() {
|
let fn_args = if let Some(cont_inner) = cont_inner_iter.next() {
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,8 @@ impl From<Expr> for QueryFragment {
|
||||||
(*lhs).into(),
|
(*lhs).into(),
|
||||||
Self::from_sql(") "),
|
Self::from_sql(") "),
|
||||||
op.into(),
|
op.into(),
|
||||||
|
// The RHS expression **must** be parenthesized to correctly
|
||||||
|
// reconstruct syntax like `= any (array[...])`.
|
||||||
Self::from_sql(" ("),
|
Self::from_sql(" ("),
|
||||||
(*rhs).into(),
|
(*rhs).into(),
|
||||||
Self::from_sql(")"),
|
Self::from_sql(")"),
|
||||||
|
|
@ -176,27 +178,44 @@ impl From<Expr> for QueryFragment {
|
||||||
fragment.push(Self::from_sql(")"));
|
fragment.push(Self::from_sql(")"));
|
||||||
fragment
|
fragment
|
||||||
}
|
}
|
||||||
|
Expr::Array(arr_items) => Self::concat([
|
||||||
|
Self::from_sql("array["),
|
||||||
|
Self::join(
|
||||||
|
arr_items.into_iter().map(|item| {
|
||||||
|
Self::concat([Self::from_sql("("), item.into(), Self::from_sql(")")])
|
||||||
|
}),
|
||||||
|
Self::from_sql(", "),
|
||||||
|
),
|
||||||
|
Self::from_sql("]"),
|
||||||
|
]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<InfixOp> for QueryFragment {
|
impl From<InfixOp> for QueryFragment {
|
||||||
fn from(value: InfixOp) -> Self {
|
fn from(value: InfixOp) -> Self {
|
||||||
Self::from_sql(match value {
|
match value {
|
||||||
InfixOp::Add => "+",
|
InfixOp::Add => Self::from_sql("+"),
|
||||||
InfixOp::Concat => "||",
|
InfixOp::Concat => Self::from_sql("||"),
|
||||||
InfixOp::Div => "/",
|
InfixOp::Div => Self::from_sql("/"),
|
||||||
InfixOp::Mult => "*",
|
InfixOp::Mult => Self::from_sql("*"),
|
||||||
InfixOp::Sub => "-",
|
InfixOp::Sub => Self::from_sql("-"),
|
||||||
InfixOp::And => "and",
|
InfixOp::And => Self::from_sql("and"),
|
||||||
InfixOp::Or => "or",
|
InfixOp::Or => Self::from_sql("or"),
|
||||||
InfixOp::Eq => "=",
|
InfixOp::Eq => Self::from_sql("="),
|
||||||
InfixOp::Gt => ">",
|
InfixOp::Gt => Self::from_sql(">"),
|
||||||
InfixOp::Gte => ">=",
|
InfixOp::Gte => Self::from_sql(">="),
|
||||||
InfixOp::Lt => "<",
|
InfixOp::Lt => Self::from_sql("<"),
|
||||||
InfixOp::Lte => "<=",
|
InfixOp::Lte => Self::from_sql("<="),
|
||||||
InfixOp::Neq => "<>",
|
InfixOp::Neq => Self::from_sql("<>"),
|
||||||
})
|
InfixOp::Cast => Self::from_sql("::"),
|
||||||
|
InfixOp::WithCmpModifierAny(inner) => {
|
||||||
|
Self::concat([(*inner).into(), Self::from_sql(" any")])
|
||||||
|
}
|
||||||
|
InfixOp::WithCmpModifierAll(inner) => {
|
||||||
|
Self::concat([(*inner).into(), Self::from_sql(" all")])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -270,33 +270,11 @@ fn into_safe_filter_sql(expr_text: &str) -> Option<QueryFragment> {
|
||||||
|
|
||||||
fn is_safe_filter_expr(expr: &Expr) -> bool {
|
fn is_safe_filter_expr(expr: &Expr) -> bool {
|
||||||
match expr {
|
match expr {
|
||||||
&Expr::Literal(_) | &Expr::ObjName(_) => true,
|
Expr::Literal(_) | &Expr::ObjName(_) => true,
|
||||||
&Expr::Infix {
|
Expr::Infix { lhs, op, rhs } => {
|
||||||
ref lhs,
|
is_safe_filter_infix_op(op) && is_safe_filter_expr(lhs) && is_safe_filter_expr(rhs)
|
||||||
op,
|
}
|
||||||
ref rhs,
|
Expr::FnCall { name, args } => match name
|
||||||
} => match op {
|
|
||||||
// Most if not all infix operators should be safe, but enumerate
|
|
||||||
// them just in case.
|
|
||||||
|
|
||||||
// Arithmetic:
|
|
||||||
InfixOp::Add
|
|
||||||
| InfixOp::Concat
|
|
||||||
| InfixOp::Div
|
|
||||||
| InfixOp::Mult
|
|
||||||
| InfixOp::Sub
|
|
||||||
// Boolean:
|
|
||||||
| InfixOp::And
|
|
||||||
| InfixOp::Or
|
|
||||||
| InfixOp::Eq
|
|
||||||
| InfixOp::Gt
|
|
||||||
| InfixOp::Gte
|
|
||||||
| InfixOp::Lt
|
|
||||||
| InfixOp::Lte
|
|
||||||
| InfixOp::Neq => is_safe_filter_expr(lhs) && is_safe_filter_expr(rhs),
|
|
||||||
_ => false,
|
|
||||||
},
|
|
||||||
&Expr::FnCall { ref name, ref args } => match name
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| s.as_str())
|
.map(|s| s.as_str())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
|
@ -339,11 +317,35 @@ fn is_safe_filter_expr(expr: &Expr) -> bool {
|
||||||
},
|
},
|
||||||
_ => false,
|
_ => false,
|
||||||
},
|
},
|
||||||
&Expr::Not(ref inner) => is_safe_filter_expr(inner),
|
Expr::Not(inner) => is_safe_filter_expr(inner),
|
||||||
&Expr::Nullness {
|
Expr::Nullness {
|
||||||
is_null: _,
|
is_null: _,
|
||||||
expr: ref inner,
|
expr: inner,
|
||||||
} => is_safe_filter_expr(inner),
|
} => is_safe_filter_expr(inner),
|
||||||
|
Expr::Array(arr_items) => arr_items.iter().all(is_safe_filter_expr),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_safe_filter_infix_op(op: &InfixOp) -> bool {
|
||||||
|
match op {
|
||||||
|
InfixOp::Add
|
||||||
|
| InfixOp::Concat
|
||||||
|
| InfixOp::Div
|
||||||
|
| InfixOp::Mult
|
||||||
|
| InfixOp::Sub
|
||||||
|
// Boolean:
|
||||||
|
| InfixOp::And
|
||||||
|
| InfixOp::Or
|
||||||
|
| InfixOp::Eq
|
||||||
|
| InfixOp::Gt
|
||||||
|
| InfixOp::Gte
|
||||||
|
| InfixOp::Lt
|
||||||
|
| InfixOp::Lte
|
||||||
|
| InfixOp::Neq
|
||||||
|
// Miscellaneous:
|
||||||
|
| InfixOp::Cast => true,
|
||||||
|
InfixOp::WithCmpModifierAny(inner) | InfixOp::WithCmpModifierAll(inner) => is_safe_filter_infix_op(inner),
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue