Add support for PostgreSQL/Redshift geometric operators (#1723)

This commit is contained in:
benrsatori 2025-02-20 20:50:32 +02:00 committed by GitHub
parent 97f0be6991
commit 339239d0c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 809 additions and 26 deletions

View file

@ -386,6 +386,10 @@ pub enum DataType {
///
/// [bigquery]: https://cloud.google.com/bigquery/docs/user-defined-functions#templated-sql-udf-parameters
AnyType,
/// geometric type
///
/// [Postgres]: https://www.postgresql.org/docs/9.5/functions-geometry.html
GeometricType(GeometricTypeKind),
}
impl fmt::Display for DataType {
@ -639,6 +643,7 @@ impl fmt::Display for DataType {
DataType::Trigger => write!(f, "TRIGGER"),
DataType::AnyType => write!(f, "ANY TYPE"),
DataType::Table(fields) => write!(f, "TABLE({})", display_comma_separated(fields)),
DataType::GeometricType(kind) => write!(f, "{}", kind),
}
}
}
@ -915,3 +920,34 @@ pub enum ArrayElemTypeDef {
/// `Array(Int64)`
Parenthesis(Box<DataType>),
}
/// Represents different types of geometric shapes which are commonly used in
/// PostgreSQL/Redshift for spatial operations and geometry-related computations.
///
/// [Postgres]: https://www.postgresql.org/docs/9.5/functions-geometry.html
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum GeometricTypeKind {
Point,
Line,
LineSegment,
GeometricBox,
GeometricPath,
Polygon,
Circle,
}
impl fmt::Display for GeometricTypeKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
GeometricTypeKind::Point => write!(f, "point"),
GeometricTypeKind::Line => write!(f, "line"),
GeometricTypeKind::LineSegment => write!(f, "lseg"),
GeometricTypeKind::GeometricBox => write!(f, "box"),
GeometricTypeKind::GeometricPath => write!(f, "path"),
GeometricTypeKind::Polygon => write!(f, "polygon"),
GeometricTypeKind::Circle => write!(f, "circle"),
}
}
}

View file

@ -95,6 +95,8 @@ use crate::ast::helpers::stmt_data_loading::{
#[cfg(feature = "visitor")]
pub use visitor::*;
pub use self::data_type::GeometricTypeKind;
mod data_type;
mod dcl;
mod ddl;
@ -1513,7 +1515,15 @@ impl fmt::Display for Expr {
Expr::UnaryOp { op, expr } => {
if op == &UnaryOperator::PGPostfixFactorial {
write!(f, "{expr}{op}")
} else if op == &UnaryOperator::Not {
} else if matches!(
op,
UnaryOperator::Not
| UnaryOperator::Hash
| UnaryOperator::AtDashAt
| UnaryOperator::DoubleAt
| UnaryOperator::QuestionDash
| UnaryOperator::QuestionPipe
) {
write!(f, "{op} {expr}")
} else {
write!(f, "{op}{expr}")

View file

@ -53,6 +53,21 @@ pub enum UnaryOperator {
PGAbs,
/// Unary logical not operator: e.g. `! false` (Hive-specific)
BangNot,
/// `#` Number of points in path or polygon (PostgreSQL/Redshift geometric operator)
/// see <https://www.postgresql.org/docs/9.5/functions-geometry.html>
Hash,
/// `@-@` Length or circumference (PostgreSQL/Redshift geometric operator)
/// see <https://www.postgresql.org/docs/9.5/functions-geometry.html>
AtDashAt,
/// `@@` Center (PostgreSQL/Redshift geometric operator)
/// see <https://www.postgresql.org/docs/9.5/functions-geometry.html>
DoubleAt,
/// `?-` Is horizontal? (PostgreSQL/Redshift geometric operator)
/// see <https://www.postgresql.org/docs/9.5/functions-geometry.html>
QuestionDash,
/// `?|` Is vertical? (PostgreSQL/Redshift geometric operator)
/// see <https://www.postgresql.org/docs/9.5/functions-geometry.html>
QuestionPipe,
}
impl fmt::Display for UnaryOperator {
@ -68,6 +83,11 @@ impl fmt::Display for UnaryOperator {
UnaryOperator::PGPrefixFactorial => "!!",
UnaryOperator::PGAbs => "@",
UnaryOperator::BangNot => "!",
UnaryOperator::Hash => "#",
UnaryOperator::AtDashAt => "@-@",
UnaryOperator::DoubleAt => "@@",
UnaryOperator::QuestionDash => "?-",
UnaryOperator::QuestionPipe => "?|",
})
}
}
@ -253,6 +273,54 @@ pub enum BinaryOperator {
/// Specifies a test for an overlap between two datetime periods:
/// <https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#overlaps-predicate>
Overlaps,
/// `##` Point of closest proximity (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
DoubleHash,
/// `<->` Distance between (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
LtDashGt,
/// `&<` Overlaps to left? (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
AndLt,
/// `&>` Overlaps to right? (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
AndGt,
/// `<<|` Is strictly below? (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
LtLtPipe,
/// `|>>` Is strictly above? (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
PipeGtGt,
/// `&<|` Does not extend above? (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
AndLtPipe,
/// `|&>` Does not extend below? (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
PipeAndGt,
/// `<^` Is below? (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
LtCaret,
/// `>^` Is above? (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
GtCaret,
/// `?#` Intersects? (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
QuestionHash,
/// `?-` Is horizontal? (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
QuestionDash,
/// `?-|` Is perpendicular? (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
QuestionDashPipe,
/// `?||` Are Parallel? (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
QuestionDoublePipe,
/// `@` Contained or on? (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
At,
/// `~=` Same as? (PostgreSQL/Redshift geometric operator)
/// See <https://www.postgresql.org/docs/9.5/functions-geometry.html>
TildeEq,
}
impl fmt::Display for BinaryOperator {
@ -310,6 +378,22 @@ impl fmt::Display for BinaryOperator {
write!(f, "OPERATOR({})", display_separated(idents, "."))
}
BinaryOperator::Overlaps => f.write_str("OVERLAPS"),
BinaryOperator::DoubleHash => f.write_str("##"),
BinaryOperator::LtDashGt => f.write_str("<->"),
BinaryOperator::AndLt => f.write_str("&<"),
BinaryOperator::AndGt => f.write_str("&>"),
BinaryOperator::LtLtPipe => f.write_str("<<|"),
BinaryOperator::PipeGtGt => f.write_str("|>>"),
BinaryOperator::AndLtPipe => f.write_str("&<|"),
BinaryOperator::PipeAndGt => f.write_str("|&>"),
BinaryOperator::LtCaret => f.write_str("<^"),
BinaryOperator::GtCaret => f.write_str(">^"),
BinaryOperator::QuestionHash => f.write_str("?#"),
BinaryOperator::QuestionDash => f.write_str("?-"),
BinaryOperator::QuestionDashPipe => f.write_str("?-|"),
BinaryOperator::QuestionDoublePipe => f.write_str("?||"),
BinaryOperator::At => f.write_str("@"),
BinaryOperator::TildeEq => f.write_str("~="),
}
}
}

View file

@ -599,18 +599,34 @@ pub trait Dialect: Debug + Any {
| Token::ExclamationMarkDoubleTilde
| Token::ExclamationMarkDoubleTildeAsterisk
| Token::Spaceship => Ok(p!(Eq)),
Token::Pipe => Ok(p!(Pipe)),
Token::Pipe
| Token::QuestionMarkDash
| Token::DoubleSharp
| Token::Overlap
| Token::AmpersandLeftAngleBracket
| Token::AmpersandRightAngleBracket
| Token::QuestionMarkDashVerticalBar
| Token::AmpersandLeftAngleBracketVerticalBar
| Token::VerticalBarAmpersandRightAngleBracket
| Token::TwoWayArrow
| Token::LeftAngleBracketCaret
| Token::RightAngleBracketCaret
| Token::QuestionMarkSharp
| Token::QuestionMarkDoubleVerticalBar
| Token::QuestionPipe
| Token::TildeEqual
| Token::AtSign
| Token::ShiftLeftVerticalBar
| Token::VerticalBarShiftRight => Ok(p!(Pipe)),
Token::Caret | Token::Sharp | Token::ShiftRight | Token::ShiftLeft => Ok(p!(Caret)),
Token::Ampersand => Ok(p!(Ampersand)),
Token::Plus | Token::Minus => Ok(p!(PlusMinus)),
Token::Mul | Token::Div | Token::DuckIntDiv | Token::Mod | Token::StringConcat => {
Ok(p!(MulDivModOp))
}
Token::DoubleColon
| Token::ExclamationMark
| Token::LBracket
| Token::Overlap
| Token::CaretAt => Ok(p!(DoubleColon)),
Token::DoubleColon | Token::ExclamationMark | Token::LBracket | Token::CaretAt => {
Ok(p!(DoubleColon))
}
Token::Arrow
| Token::LongArrow
| Token::HashArrow
@ -622,7 +638,6 @@ pub trait Dialect: Debug + Any {
| Token::AtAt
| Token::Question
| Token::QuestionAnd
| Token::QuestionPipe
| Token::CustomBinaryOperator(_) => Ok(p!(PgOther)),
_ => Ok(self.prec_unknown()),
}
@ -921,6 +936,13 @@ pub trait Dialect: Debug + Any {
fn supports_array_typedef_size(&self) -> bool {
false
}
/// Returns true if the dialect supports geometric types.
///
/// Postgres: <https://www.postgresql.org/docs/9.5/functions-geometry.html>
/// e.g. @@ circle '((0,0),10)'
fn supports_geometric_types(&self) -> bool {
false
}
}
/// This represents the operators for which precedence must be defined

View file

@ -250,4 +250,8 @@ impl Dialect for PostgreSqlDialect {
fn supports_array_typedef_size(&self) -> bool {
true
}
fn supports_geometric_types(&self) -> bool {
true
}
}

View file

@ -113,4 +113,8 @@ impl Dialect for RedshiftSqlDialect {
fn supports_string_escape_constant(&self) -> bool {
true
}
fn supports_geometric_types(&self) -> bool {
true
}
}

View file

@ -141,6 +141,7 @@ define_keywords!(
BOOL,
BOOLEAN,
BOTH,
BOX,
BROWSE,
BTREE,
BUCKET,
@ -175,6 +176,7 @@ define_keywords!(
CHARSET,
CHAR_LENGTH,
CHECK,
CIRCLE,
CLEAR,
CLOB,
CLONE,
@ -478,6 +480,7 @@ define_keywords!(
LIKE,
LIKE_REGEX,
LIMIT,
LINE,
LINES,
LIST,
LISTEN,
@ -499,6 +502,7 @@ define_keywords!(
LOWER,
LOW_PRIORITY,
LS,
LSEG,
MACRO,
MANAGE,
MANAGED,
@ -653,7 +657,9 @@ define_keywords!(
PLACING,
PLAN,
PLANS,
POINT,
POLICY,
POLYGON,
POOL,
PORTION,
POSITION,

View file

@ -1220,7 +1220,17 @@ impl<'a> Parser<'a> {
Keyword::MAP if *self.peek_token_ref() == Token::LBrace && self.dialect.support_map_literal_syntax() => {
Ok(Some(self.parse_duckdb_map_literal()?))
}
_ => Ok(None)
_ if self.dialect.supports_geometric_types() => match w.keyword {
Keyword::CIRCLE => Ok(Some(self.parse_geometric_type(GeometricTypeKind::Circle)?)),
Keyword::BOX => Ok(Some(self.parse_geometric_type(GeometricTypeKind::GeometricBox)?)),
Keyword::PATH => Ok(Some(self.parse_geometric_type(GeometricTypeKind::GeometricPath)?)),
Keyword::LINE => Ok(Some(self.parse_geometric_type(GeometricTypeKind::Line)?)),
Keyword::LSEG => Ok(Some(self.parse_geometric_type(GeometricTypeKind::LineSegment)?)),
Keyword::POINT => Ok(Some(self.parse_geometric_type(GeometricTypeKind::Point)?)),
Keyword::POLYGON => Ok(Some(self.parse_geometric_type(GeometricTypeKind::Polygon)?)),
_ => Ok(None),
},
_ => Ok(None),
}
}
@ -1400,6 +1410,33 @@ impl<'a> Parser<'a> {
),
})
}
tok @ Token::Sharp
| tok @ Token::AtDashAt
| tok @ Token::AtAt
| tok @ Token::QuestionMarkDash
| tok @ Token::QuestionPipe
if self.dialect.supports_geometric_types() =>
{
let op = match tok {
Token::Sharp => UnaryOperator::Hash,
Token::AtDashAt => UnaryOperator::AtDashAt,
Token::AtAt => UnaryOperator::DoubleAt,
Token::QuestionMarkDash => UnaryOperator::QuestionDash,
Token::QuestionPipe => UnaryOperator::QuestionPipe,
_ => {
return Err(ParserError::ParserError(format!(
"Unexpected token in unary operator parsing: {:?}",
tok
)))
}
};
Ok(Expr::UnaryOp {
op,
expr: Box::new(
self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?,
),
})
}
Token::EscapedStringLiteral(_) if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) =>
{
self.prev_token();
@ -1465,6 +1502,14 @@ impl<'a> Parser<'a> {
}
}
fn parse_geometric_type(&mut self, kind: GeometricTypeKind) -> Result<Expr, ParserError> {
let value: Value = self.parse_value()?;
Ok(Expr::TypedString {
data_type: DataType::GeometricType(kind),
value,
})
}
/// Try to parse an [Expr::CompoundFieldAccess] like `a.b.c` or `a.b[1].c`.
/// If all the fields are `Expr::Identifier`s, return an [Expr::CompoundIdentifier] instead.
/// If only the root exists, return the root.
@ -3070,15 +3115,18 @@ impl<'a> Parser<'a> {
Token::DuckIntDiv if dialect_is!(dialect is DuckDbDialect | GenericDialect) => {
Some(BinaryOperator::DuckIntegerDivide)
}
Token::ShiftLeft if dialect_is!(dialect is PostgreSqlDialect | DuckDbDialect | GenericDialect) => {
Token::ShiftLeft if dialect_is!(dialect is PostgreSqlDialect | DuckDbDialect | GenericDialect | RedshiftSqlDialect) => {
Some(BinaryOperator::PGBitwiseShiftLeft)
}
Token::ShiftRight if dialect_is!(dialect is PostgreSqlDialect | DuckDbDialect | GenericDialect) => {
Token::ShiftRight if dialect_is!(dialect is PostgreSqlDialect | DuckDbDialect | GenericDialect | RedshiftSqlDialect) => {
Some(BinaryOperator::PGBitwiseShiftRight)
}
Token::Sharp if dialect_is!(dialect is PostgreSqlDialect) => {
Token::Sharp if dialect_is!(dialect is PostgreSqlDialect | RedshiftSqlDialect) => {
Some(BinaryOperator::PGBitwiseXor)
}
Token::Overlap if dialect_is!(dialect is PostgreSqlDialect | RedshiftSqlDialect) => {
Some(BinaryOperator::PGOverlap)
}
Token::Overlap if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => {
Some(BinaryOperator::PGOverlap)
}
@ -3106,6 +3154,57 @@ impl<'a> Parser<'a> {
Token::QuestionAnd => Some(BinaryOperator::QuestionAnd),
Token::QuestionPipe => Some(BinaryOperator::QuestionPipe),
Token::CustomBinaryOperator(s) => Some(BinaryOperator::Custom(s.clone())),
Token::DoubleSharp if self.dialect.supports_geometric_types() => {
Some(BinaryOperator::DoubleHash)
}
Token::AmpersandLeftAngleBracket if self.dialect.supports_geometric_types() => {
Some(BinaryOperator::AndLt)
}
Token::AmpersandRightAngleBracket if self.dialect.supports_geometric_types() => {
Some(BinaryOperator::AndGt)
}
Token::QuestionMarkDash if self.dialect.supports_geometric_types() => {
Some(BinaryOperator::QuestionDash)
}
Token::AmpersandLeftAngleBracketVerticalBar
if self.dialect.supports_geometric_types() =>
{
Some(BinaryOperator::AndLtPipe)
}
Token::VerticalBarAmpersandRightAngleBracket
if self.dialect.supports_geometric_types() =>
{
Some(BinaryOperator::PipeAndGt)
}
Token::TwoWayArrow if self.dialect.supports_geometric_types() => {
Some(BinaryOperator::LtDashGt)
}
Token::LeftAngleBracketCaret if self.dialect.supports_geometric_types() => {
Some(BinaryOperator::LtCaret)
}
Token::RightAngleBracketCaret if self.dialect.supports_geometric_types() => {
Some(BinaryOperator::GtCaret)
}
Token::QuestionMarkSharp if self.dialect.supports_geometric_types() => {
Some(BinaryOperator::QuestionHash)
}
Token::QuestionMarkDoubleVerticalBar if self.dialect.supports_geometric_types() => {
Some(BinaryOperator::QuestionDoublePipe)
}
Token::QuestionMarkDashVerticalBar if self.dialect.supports_geometric_types() => {
Some(BinaryOperator::QuestionDashPipe)
}
Token::TildeEqual if self.dialect.supports_geometric_types() => {
Some(BinaryOperator::TildeEq)
}
Token::ShiftLeftVerticalBar if self.dialect.supports_geometric_types() => {
Some(BinaryOperator::LtLtPipe)
}
Token::VerticalBarShiftRight if self.dialect.supports_geometric_types() => {
Some(BinaryOperator::PipeGtGt)
}
Token::AtSign if self.dialect.supports_geometric_types() => Some(BinaryOperator::At),
Token::Word(w) => match w.keyword {
Keyword::AND => Some(BinaryOperator::And),

View file

@ -170,8 +170,10 @@ pub enum Token {
RBrace,
/// Right Arrow `=>`
RArrow,
/// Sharp `#` used for PostgreSQL Bitwise XOR operator
/// Sharp `#` used for PostgreSQL Bitwise XOR operator, also PostgreSQL/Redshift geometrical unary/binary operator (Number of points in path or polygon/Intersection)
Sharp,
/// `##` PostgreSQL/Redshift geometrical binary operator (Point of closest proximity)
DoubleSharp,
/// Tilde `~` used for PostgreSQL Bitwise NOT operator or case sensitive match regular expression operator
Tilde,
/// `~*` , a case insensitive match regular expression operator in PostgreSQL
@ -198,7 +200,7 @@ pub enum Token {
ExclamationMark,
/// Double Exclamation Mark `!!` used for PostgreSQL prefix factorial operator
DoubleExclamationMark,
/// AtSign `@` used for PostgreSQL abs operator
/// AtSign `@` used for PostgreSQL abs operator, also PostgreSQL/Redshift geometrical unary/binary operator (Center, Contained or on)
AtSign,
/// `^@`, a "starts with" string operator in PostgreSQL
CaretAt,
@ -214,6 +216,36 @@ pub enum Token {
LongArrow,
/// `#>`, extracts JSON sub-object at the specified path
HashArrow,
/// `@-@` PostgreSQL/Redshift geometrical unary operator (Length or circumference)
AtDashAt,
/// `?-` PostgreSQL/Redshift geometrical unary/binary operator (Is horizontal?/Are horizontally aligned?)
QuestionMarkDash,
/// `&<` PostgreSQL/Redshift geometrical binary operator (Overlaps to left?)
AmpersandLeftAngleBracket,
/// `&>` PostgreSQL/Redshift geometrical binary operator (Overlaps to right?)`
AmpersandRightAngleBracket,
/// `&<|` PostgreSQL/Redshift geometrical binary operator (Does not extend above?)`
AmpersandLeftAngleBracketVerticalBar,
/// `|&>` PostgreSQL/Redshift geometrical binary operator (Does not extend below?)`
VerticalBarAmpersandRightAngleBracket,
/// `<->` PostgreSQL/Redshift geometrical binary operator (Distance between)
TwoWayArrow,
/// `<^` PostgreSQL/Redshift geometrical binary operator (Is below?)
LeftAngleBracketCaret,
/// `>^` PostgreSQL/Redshift geometrical binary operator (Is above?)
RightAngleBracketCaret,
/// `?#` PostgreSQL/Redshift geometrical binary operator (Intersects or overlaps)
QuestionMarkSharp,
/// `?-|` PostgreSQL/Redshift geometrical binary operator (Is perpendicular?)
QuestionMarkDashVerticalBar,
/// `?||` PostgreSQL/Redshift geometrical binary operator (Are parallel?)
QuestionMarkDoubleVerticalBar,
/// `~=` PostgreSQL/Redshift geometrical binary operator (Same as)
TildeEqual,
/// `<<| PostgreSQL/Redshift geometrical binary operator (Is strictly below?)
ShiftLeftVerticalBar,
/// `|>> PostgreSQL/Redshift geometrical binary operator (Is strictly above?)
VerticalBarShiftRight,
/// `#>>`, extracts JSON sub-object at the specified path as text
HashLongArrow,
/// jsonb @> jsonb -> boolean: Test whether left json contains the right json
@ -303,6 +335,7 @@ impl fmt::Display for Token {
Token::RBrace => f.write_str("}"),
Token::RArrow => f.write_str("=>"),
Token::Sharp => f.write_str("#"),
Token::DoubleSharp => f.write_str("##"),
Token::ExclamationMark => f.write_str("!"),
Token::DoubleExclamationMark => f.write_str("!!"),
Token::Tilde => f.write_str("~"),
@ -320,6 +353,21 @@ impl fmt::Display for Token {
Token::Overlap => f.write_str("&&"),
Token::PGSquareRoot => f.write_str("|/"),
Token::PGCubeRoot => f.write_str("||/"),
Token::AtDashAt => f.write_str("@-@"),
Token::QuestionMarkDash => f.write_str("?-"),
Token::AmpersandLeftAngleBracket => f.write_str("&<"),
Token::AmpersandRightAngleBracket => f.write_str("&>"),
Token::AmpersandLeftAngleBracketVerticalBar => f.write_str("&<|"),
Token::VerticalBarAmpersandRightAngleBracket => f.write_str("|&>"),
Token::TwoWayArrow => f.write_str("<->"),
Token::LeftAngleBracketCaret => f.write_str("<^"),
Token::RightAngleBracketCaret => f.write_str(">^"),
Token::QuestionMarkSharp => f.write_str("?#"),
Token::QuestionMarkDashVerticalBar => f.write_str("?-|"),
Token::QuestionMarkDoubleVerticalBar => f.write_str("?||"),
Token::TildeEqual => f.write_str("~="),
Token::ShiftLeftVerticalBar => f.write_str("<<|"),
Token::VerticalBarShiftRight => f.write_str("|>>"),
Token::Placeholder(ref s) => write!(f, "{s}"),
Token::Arrow => write!(f, "->"),
Token::LongArrow => write!(f, "->>"),
@ -1308,6 +1356,28 @@ impl<'a> Tokenizer<'a> {
_ => self.start_binop(chars, "||", Token::StringConcat),
}
}
Some('&') if self.dialect.supports_geometric_types() => {
chars.next(); // consume
match chars.peek() {
Some('>') => self.consume_for_binop(
chars,
"|&>",
Token::VerticalBarAmpersandRightAngleBracket,
),
_ => self.start_binop_opt(chars, "|&", None),
}
}
Some('>') if self.dialect.supports_geometric_types() => {
chars.next(); // consume
match chars.peek() {
Some('>') => self.consume_for_binop(
chars,
"|>>",
Token::VerticalBarShiftRight,
),
_ => self.start_binop_opt(chars, "|>", None),
}
}
// Bitshift '|' operator
_ => self.start_binop(chars, "|", Token::Pipe),
}
@ -1356,8 +1426,34 @@ impl<'a> Tokenizer<'a> {
_ => self.start_binop(chars, "<=", Token::LtEq),
}
}
Some('|') if self.dialect.supports_geometric_types() => {
self.consume_for_binop(chars, "<<|", Token::ShiftLeftVerticalBar)
}
Some('>') => self.consume_for_binop(chars, "<>", Token::Neq),
Some('<') if self.dialect.supports_geometric_types() => {
chars.next(); // consume
match chars.peek() {
Some('|') => self.consume_for_binop(
chars,
"<<|",
Token::ShiftLeftVerticalBar,
),
_ => self.start_binop(chars, "<<", Token::ShiftLeft),
}
}
Some('<') => self.consume_for_binop(chars, "<<", Token::ShiftLeft),
Some('-') if self.dialect.supports_geometric_types() => {
chars.next(); // consume
match chars.peek() {
Some('>') => {
self.consume_for_binop(chars, "<->", Token::TwoWayArrow)
}
_ => self.start_binop_opt(chars, "<-", None),
}
}
Some('^') if self.dialect.supports_geometric_types() => {
self.consume_for_binop(chars, "<^", Token::LeftAngleBracketCaret)
}
Some('@') => self.consume_for_binop(chars, "<@", Token::ArrowAt),
_ => self.start_binop(chars, "<", Token::Lt),
}
@ -1367,6 +1463,9 @@ impl<'a> Tokenizer<'a> {
match chars.peek() {
Some('=') => self.consume_for_binop(chars, ">=", Token::GtEq),
Some('>') => self.consume_for_binop(chars, ">>", Token::ShiftRight),
Some('^') if self.dialect.supports_geometric_types() => {
self.consume_for_binop(chars, ">^", Token::RightAngleBracketCaret)
}
_ => self.start_binop(chars, ">", Token::Gt),
}
}
@ -1385,6 +1484,22 @@ impl<'a> Tokenizer<'a> {
'&' => {
chars.next(); // consume the '&'
match chars.peek() {
Some('>') if self.dialect.supports_geometric_types() => {
chars.next();
self.consume_and_return(chars, Token::AmpersandRightAngleBracket)
}
Some('<') if self.dialect.supports_geometric_types() => {
chars.next(); // consume
match chars.peek() {
Some('|') => self.consume_and_return(
chars,
Token::AmpersandLeftAngleBracketVerticalBar,
),
_ => {
self.start_binop(chars, "&<", Token::AmpersandLeftAngleBracket)
}
}
}
Some('&') => {
chars.next(); // consume the second '&'
self.start_binop(chars, "&&", Token::Overlap)
@ -1415,6 +1530,9 @@ impl<'a> Tokenizer<'a> {
chars.next(); // consume
match chars.peek() {
Some('*') => self.consume_for_binop(chars, "~*", Token::TildeAsterisk),
Some('=') if self.dialect.supports_geometric_types() => {
self.consume_for_binop(chars, "~=", Token::TildeEqual)
}
Some('~') => {
chars.next();
match chars.peek() {
@ -1441,6 +1559,9 @@ impl<'a> Tokenizer<'a> {
}
}
Some(' ') => Ok(Some(Token::Sharp)),
Some('#') if self.dialect.supports_geometric_types() => {
self.consume_for_binop(chars, "##", Token::DoubleSharp)
}
Some(sch) if self.dialect.is_identifier_start('#') => {
self.tokenize_identifier_or_keyword([ch, *sch], chars)
}
@ -1450,6 +1571,16 @@ impl<'a> Tokenizer<'a> {
'@' => {
chars.next();
match chars.peek() {
Some('@') if self.dialect.supports_geometric_types() => {
self.consume_and_return(chars, Token::AtAt)
}
Some('-') if self.dialect.supports_geometric_types() => {
chars.next();
match chars.peek() {
Some('@') => self.consume_and_return(chars, Token::AtDashAt),
_ => self.start_binop_opt(chars, "@-", None),
}
}
Some('>') => self.consume_and_return(chars, Token::AtArrow),
Some('?') => self.consume_and_return(chars, Token::AtQuestion),
Some('@') => {
@ -1482,11 +1613,30 @@ impl<'a> Tokenizer<'a> {
}
}
// Postgres uses ? for jsonb operators, not prepared statements
'?' if dialect_of!(self is PostgreSqlDialect) => {
chars.next();
'?' if self.dialect.supports_geometric_types() => {
chars.next(); // consume
match chars.peek() {
Some('|') => self.consume_and_return(chars, Token::QuestionPipe),
Some('|') => {
chars.next();
match chars.peek() {
Some('|') => self.consume_and_return(
chars,
Token::QuestionMarkDoubleVerticalBar,
),
_ => Ok(Some(Token::QuestionPipe)),
}
}
Some('&') => self.consume_and_return(chars, Token::QuestionAnd),
Some('-') => {
chars.next(); // consume
match chars.peek() {
Some('|') => self
.consume_and_return(chars, Token::QuestionMarkDashVerticalBar),
_ => Ok(Some(Token::QuestionMarkDash)),
}
}
Some('#') => self.consume_and_return(chars, Token::QuestionMarkSharp),
_ => self.consume_and_return(chars, Token::Question),
}
}
@ -1520,7 +1670,7 @@ impl<'a> Tokenizer<'a> {
default: Token,
) -> Result<Option<Token>, TokenizerError> {
chars.next(); // consume the first char
self.start_binop(chars, prefix, default)
self.start_binop_opt(chars, prefix, Some(default))
}
/// parse a custom binary operator
@ -1529,6 +1679,16 @@ impl<'a> Tokenizer<'a> {
chars: &mut State,
prefix: &str,
default: Token,
) -> Result<Option<Token>, TokenizerError> {
self.start_binop_opt(chars, prefix, Some(default))
}
/// parse a custom binary operator
fn start_binop_opt(
&self,
chars: &mut State,
prefix: &str,
default: Option<Token>,
) -> Result<Option<Token>, TokenizerError> {
let mut custom = None;
while let Some(&ch) = chars.peek() {
@ -1539,10 +1699,14 @@ impl<'a> Tokenizer<'a> {
custom.get_or_insert_with(|| prefix.to_string()).push(ch);
chars.next();
}
Ok(Some(
custom.map(Token::CustomBinaryOperator).unwrap_or(default),
))
match (custom, default) {
(Some(custom), _) => Ok(Token::CustomBinaryOperator(custom).into()),
(None, Some(tok)) => Ok(Some(tok)),
(None, None) => self.tokenizer_error(
chars.location(),
format!("Expected a valid binary operator after '{}'", prefix),
),
}
}
/// Tokenize dollar preceded value (i.e: a string/placeholder)

View file

@ -13860,3 +13860,361 @@ fn test_select_from_first() {
assert_eq!(ast.to_string(), q);
}
}
#[test]
fn test_geometric_unary_operators() {
// Number of points in path or polygon
let sql = "# path '((1,0),(0,1),(-1,0))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::UnaryOp {
op: UnaryOperator::Hash,
..
}
));
// Length or circumference
let sql = "@-@ path '((0,0),(1,0))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::UnaryOp {
op: UnaryOperator::AtDashAt,
..
}
));
// Center
let sql = "@@ circle '((0,0),10)'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::UnaryOp {
op: UnaryOperator::DoubleAt,
..
}
));
// Is horizontal?
let sql = "?- lseg '((-1,0),(1,0))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::UnaryOp {
op: UnaryOperator::QuestionDash,
..
}
));
// Is vertical?
let sql = "?| lseg '((-1,0),(1,0))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::UnaryOp {
op: UnaryOperator::QuestionPipe,
..
}
));
}
#[test]
fn test_geomtery_type() {
let sql = "point '1,2'";
assert_eq!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::Point),
value: Value::SingleQuotedString("1,2".to_string()),
}
);
let sql = "line '1,2,3,4'";
assert_eq!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::Line),
value: Value::SingleQuotedString("1,2,3,4".to_string()),
}
);
let sql = "path '1,2,3,4'";
assert_eq!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::GeometricPath),
value: Value::SingleQuotedString("1,2,3,4".to_string()),
}
);
let sql = "box '1,2,3,4'";
assert_eq!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::GeometricBox),
value: Value::SingleQuotedString("1,2,3,4".to_string()),
}
);
let sql = "circle '1,2,3'";
assert_eq!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::Circle),
value: Value::SingleQuotedString("1,2,3".to_string()),
}
);
let sql = "polygon '1,2,3,4'";
assert_eq!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::Polygon),
value: Value::SingleQuotedString("1,2,3,4".to_string()),
}
);
let sql = "lseg '1,2,3,4'";
assert_eq!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::LineSegment),
value: Value::SingleQuotedString("1,2,3,4".to_string()),
}
);
}
#[test]
fn test_geometric_binary_operators() {
// Translation plus
let sql = "box '((0,0),(1,1))' + point '(2.0,0)'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::Plus,
..
}
));
// Translation minus
let sql = "box '((0,0),(1,1))' - point '(2.0,0)'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::Minus,
..
}
));
// Scaling multiply
let sql = "box '((0,0),(1,1))' * point '(2.0,0)'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::Multiply,
..
}
));
// Scaling divide
let sql = "box '((0,0),(1,1))' / point '(2.0,0)'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::Divide,
..
}
));
// Intersection
let sql = "'((1,-1),(-1,1))' # '((1,1),(-1,-1))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::PGBitwiseXor,
..
}
));
//Point of closest proximity
let sql = "point '(0,0)' ## lseg '((2,0),(0,2))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::DoubleHash,
..
}
));
// Point of closest proximity
let sql = "box '((0,0),(1,1))' && box '((0,0),(2,2))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::PGOverlap,
..
}
));
// Overlaps to left?
let sql = "box '((0,0),(1,1))' &< box '((0,0),(2,2))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::AndLt,
..
}
));
// Overlaps to right?
let sql = "box '((0,0),(3,3))' &> box '((0,0),(2,2))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::AndGt,
..
}
));
// Distance between
let sql = "circle '((0,0),1)' <-> circle '((5,0),1)'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::LtDashGt,
..
}
));
// Is left of?
let sql = "circle '((0,0),1)' << circle '((5,0),1)'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::PGBitwiseShiftLeft,
..
}
));
// Is right of?
let sql = "circle '((5,0),1)' >> circle '((0,0),1)'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::PGBitwiseShiftRight,
..
}
));
// Is below?
let sql = "circle '((0,0),1)' <^ circle '((0,5),1)'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::LtCaret,
..
}
));
// Intersects or overlaps
let sql = "lseg '((-1,0),(1,0))' ?# box '((-2,-2),(2,2))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::QuestionHash,
..
}
));
// Is horizontal?
let sql = "point '(1,0)' ?- point '(0,0)'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::QuestionDash,
..
}
));
// Is perpendicular?
let sql = "lseg '((0,0),(0,1))' ?-| lseg '((0,0),(1,0))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::QuestionDashPipe,
..
}
));
// Is vertical?
let sql = "point '(0,1)' ?| point '(0,0)'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::QuestionPipe,
..
}
));
// Are parallel?
let sql = "lseg '((-1,0),(1,0))' ?|| lseg '((-1,2),(1,2))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::QuestionDoublePipe,
..
}
));
// Contained or on?
let sql = "point '(1,1)' @ circle '((0,0),2)'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::At,
..
}
));
//
// Same as?
let sql = "polygon '((0,0),(1,1))' ~= polygon '((1,1),(0,0))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::TildeEq,
..
}
));
// Is strictly below?
let sql = "box '((0,0),(3,3))' <<| box '((3,4),(5,5))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::LtLtPipe,
..
}
));
// Is strictly above?
let sql = "box '((3,4),(5,5))' |>> box '((0,0),(3,3))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::PipeGtGt,
..
}
));
// Does not extend above?
let sql = "box '((0,0),(1,1))' &<| box '((0,0),(2,2))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::AndLtPipe,
..
}
));
// Does not extend below?
let sql = "box '((0,0),(3,3))' |&> box '((0,0),(2,2))'";
assert!(matches!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::BinaryOp {
op: BinaryOperator::PipeAndGt,
..
}
));
}

View file

@ -2066,12 +2066,8 @@ fn parse_pg_custom_binary_ops() {
let operators = [
// PostGIS
"&&&", // n-D bounding boxes intersect
"&<", // (is strictly to the left of)
"&>", // (is strictly to the right of)
"|=|", // distance between A and B trajectories at their closest point of approach
"<<#>>", // n-D distance between A and B bounding boxes
"|>>", // A's bounding box is strictly above B's.
"~=", // bounding box is the same
// PGroonga
"&@", // Full text search by a keyword
"&@~", // Full text search by easy to use query language