Skip to content
Open
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
5 changes: 5 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1990,6 +1990,11 @@ pub trait Dialect: Debug + Any {
false
}

/// Whether a custom type name may introduce a typed string.
fn is_custom_type_typed_string(&self, _first_keyword: Keyword, _qualified: bool) -> bool {
false
}

/// Returns true if the dialect supports aliased function arguments,
/// e.g. `XMLFOREST(a AS x)` in PostgreSQL.
fn supports_aliased_function_args(&self) -> bool {
Expand Down
162 changes: 162 additions & 0 deletions src/dialect/postgresql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,157 @@ const RESERVED_EXCLUSIONS_FOR_TABLE_ALIAS: &[Keyword] = &[
Keyword::VIEW,
];

const RESERVED_TYPE_NAME_KEYWORDS: &[Keyword] = &[
Keyword::ALL,
Keyword::ANALYZE,
Keyword::AND,
Keyword::ANY,
Keyword::ARRAY,
Keyword::AS,
Keyword::ASC,
Keyword::ASYMMETRIC,
Keyword::BOTH,
Keyword::CASE,
Keyword::CAST,
Keyword::CHECK,
Keyword::COLLATE,
Keyword::COLUMN,
Keyword::CONSTRAINT,
Keyword::CREATE,
Keyword::CURRENT_CATALOG,
Keyword::CURRENT_DATE,
Keyword::CURRENT_ROLE,
Keyword::CURRENT_TIME,
Keyword::CURRENT_TIMESTAMP,
Keyword::CURRENT_USER,
Keyword::DEFAULT,
Keyword::DEFERRABLE,
Keyword::DESC,
Keyword::DISTINCT,
Keyword::DO,
Keyword::ELSE,
Keyword::END,
Keyword::EXCEPT,
Keyword::FALSE,
Keyword::FETCH,
Keyword::FOR,
Keyword::FOREIGN,
Keyword::FROM,
Keyword::GRANT,
Keyword::GROUP,
Keyword::HAVING,
Keyword::IN,
Keyword::INITIALLY,
Keyword::INTERSECT,
Keyword::INTO,
Keyword::LATERAL,
Keyword::LEADING,
Keyword::LIMIT,
Keyword::LOCALTIME,
Keyword::LOCALTIMESTAMP,
Keyword::NOT,
Keyword::NULL,
Keyword::OFFSET,
Keyword::ON,
Keyword::ONLY,
Keyword::OR,
Keyword::ORDER,
Keyword::PLACING,
Keyword::PRIMARY,
Keyword::REFERENCES,
Keyword::RETURNING,
Keyword::SELECT,
Keyword::SESSION_USER,
Keyword::SOME,
Keyword::SYMMETRIC,
Keyword::SYSTEM_USER,
Keyword::TABLE,
Keyword::THEN,
Keyword::TO,
Keyword::TRAILING,
Keyword::TRUE,
Keyword::UNION,
Keyword::UNIQUE,
Keyword::USER,
Keyword::USING,
Keyword::VARIADIC,
Keyword::WHEN,
Keyword::WHERE,
Keyword::WINDOW,
Keyword::WITH,
];

const COL_NAME_KEYWORDS: &[Keyword] = &[
Keyword::BETWEEN,
Keyword::BIGINT,
Keyword::BIT,
Keyword::BOOLEAN,
Keyword::CHAR,
Keyword::CHARACTER,
Keyword::COALESCE,
Keyword::DEC,
Keyword::DECIMAL,
Keyword::EXISTS,
Keyword::EXTRACT,
Keyword::FLOAT,
Keyword::GROUPING,
Keyword::INOUT,
Keyword::INT,
Keyword::INTEGER,
Keyword::INTERVAL,
Keyword::JSON,
Keyword::JSON_TABLE,
Keyword::LEAST,
Keyword::NATIONAL,
Keyword::NCHAR,
Keyword::NONE,
Keyword::NORMALIZE,
Keyword::NULLIF,
Keyword::NUMERIC,
Keyword::OUT,
Keyword::OVERLAY,
Keyword::POSITION,
Keyword::PRECISION,
Keyword::REAL,
Keyword::ROW,
Keyword::SETOF,
Keyword::SMALLINT,
Keyword::SUBSTRING,
Keyword::TIME,
Keyword::TIMESTAMP,
Keyword::TREAT,
Keyword::TRIM,
Keyword::VALUES,
Keyword::VARCHAR,
Keyword::XMLNAMESPACES,
Keyword::XMLTABLE,
];

const TYPE_FUNC_NAME_KEYWORDS: &[Keyword] = &[
Keyword::AUTHORIZATION,
Keyword::BINARY,
Keyword::COLLATION,
Keyword::CONCURRENTLY,
Keyword::CROSS,
Keyword::CURRENT_SCHEMA,
Keyword::FREEZE,
Keyword::FULL,
Keyword::ILIKE,
Keyword::INNER,
Keyword::IS,
Keyword::JOIN,
Keyword::LEFT,
Keyword::LIKE,
Keyword::NATURAL,
Keyword::NOTNULL,
Keyword::OUTER,
Keyword::OVERLAPS,
Keyword::RIGHT,
Keyword::SIMILAR,
Keyword::TABLESAMPLE,
Keyword::VERBOSE,
];

/// A [`Dialect`] for [PostgreSQL](https://www.postgresql.org/)
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand Down Expand Up @@ -367,6 +518,17 @@ impl Dialect for PostgreSqlDialect {
true
}

/// See <https://www.postgresql.org/docs/current/sql-keywords-appendix.html>.
fn is_custom_type_typed_string(&self, first_keyword: Keyword, qualified: bool) -> bool {
let not_a_type_name = if qualified {
TYPE_FUNC_NAME_KEYWORDS
} else {
COL_NAME_KEYWORDS
};
!RESERVED_TYPE_NAME_KEYWORDS.contains(&first_keyword)
&& !not_a_type_name.contains(&first_keyword)
}

fn supports_aliased_function_args(&self) -> bool {
true
}
Expand Down
68 changes: 44 additions & 24 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1665,17 +1665,26 @@ impl<'a> Parser<'a> {
Keyword::LAMBDA if self.dialect.supports_lambda_keyword_syntax() => {
Ok(Some(self.parse_lambda_expr()?))
}
_ 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),
_ => match self.geometric_type_kind(w.keyword) {
Some(kind) => Ok(Some(self.parse_geometric_type(kind)?)),
None => Ok(None),
},
_ => Ok(None),
}
}

fn geometric_type_kind(&self, keyword: Keyword) -> Option<GeometricTypeKind> {
if !self.dialect.supports_geometric_types() {
return None;
}
match keyword {
Keyword::CIRCLE => Some(GeometricTypeKind::Circle),
Keyword::BOX => Some(GeometricTypeKind::GeometricBox),
Keyword::PATH => Some(GeometricTypeKind::GeometricPath),
Keyword::LINE => Some(GeometricTypeKind::Line),
Keyword::LSEG => Some(GeometricTypeKind::LineSegment),
Keyword::POINT => Some(GeometricTypeKind::Point),
Keyword::POLYGON => Some(GeometricTypeKind::Polygon),
_ => None,
}
}

Expand Down Expand Up @@ -1759,6 +1768,20 @@ impl<'a> Parser<'a> {
}
}

fn peek_custom_typed_string(&self, first_keyword: Keyword, name: &ObjectName) -> bool {
let qualified = name.0.len() > 1;
matches!(
self.peek_token_ref().token,
Token::SingleQuotedString(_)
| Token::EscapedStringLiteral(_)
| Token::UnicodeStringLiteral(_)
| Token::DollarQuotedString(_)
) && self
.dialect
.is_custom_type_typed_string(first_keyword, qualified)
&& (qualified || self.geometric_type_kind(first_keyword).is_none())
}

/// Parse an expression prefix.
pub fn parse_prefix(&mut self) -> Result<Expr, ParserError> {
// allow the dialect to override prefix parsing
Expand Down Expand Up @@ -1812,26 +1835,23 @@ impl<'a> Parser<'a> {
// name is not followed by a string literal, but in fact in PostgreSQL it is a valid
// expression that should parse as the column name "date".
let loc = self.peek_token_ref().span.start;
let first_keyword = match &self.peek_token_ref().token {
Token::Word(w) => Some(w.keyword),
_ => None,
};
let opt_expr = self.maybe_parse(|parser| {
match parser.parse_data_type()? {
DataType::Interval { .. } => parser.parse_interval(),
// PostgreSQL allows almost any identifier to be used as custom data type name,
// and we support that in `parse_data_type()`. But unlike Postgres we don't
// have a list of globally reserved keywords (since they vary across dialects),
// so given `NOT 'a' LIKE 'b'`, we'd accept `NOT` as a possible custom data type
// name, resulting in `NOT 'a'` being recognized as a `TypedString` instead of
// an unary negation `NOT ('a' LIKE 'b')`. To solve this, we don't accept the
// `type 'string'` syntax for the custom data types at all ...
//
// ... with the exception of `xml '...'` on dialects that support XML
// expressions, which is a valid PostgreSQL typed string literal.
DataType::Custom(ref name, ref modifiers)
// Custom names need dialect consent, else `NOT 'a'` would read as a typed string.
DataType::Custom(name, modifiers)
if modifiers.is_empty()
&& Self::is_simple_unquoted_object_name(name, "xml")
&& parser.dialect.supports_xml_expressions() =>
&& ((Self::is_simple_unquoted_object_name(&name, "xml")
&& parser.dialect.supports_xml_expressions())
|| first_keyword
.is_some_and(|kw| parser.peek_custom_typed_string(kw, &name))) =>
{
Ok(Expr::TypedString(TypedString {
data_type: DataType::Custom(name.clone(), modifiers.clone()),
data_type: DataType::Custom(name, modifiers),
value: parser.parse_value()?,
uses_odbc_syntax: false,
}))
Expand Down
4 changes: 3 additions & 1 deletion tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8503,7 +8503,9 @@ fn parse_trim() {

assert_eq!(
ParserError::ParserError("Expected: ), found: 'xyz'".to_owned()),
parse_sql_statements("SELECT TRIM(FOO 'xyz' FROM 'xyzfooxyz')").unwrap_err()
all_dialects_where(|d| !d.is_custom_type_typed_string(Keyword::NoKeyword, false))
.parse_sql_statements("SELECT TRIM(FOO 'xyz' FROM 'xyzfooxyz')")
.unwrap_err()
);

// dialects that support comma-separated TRIM syntax
Expand Down
48 changes: 48 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10202,3 +10202,51 @@ fn parse_string_literal_alias_rejected() {
}
pg().verified_stmt(r#"SELECT a AS "x" FROM t AS "y""#);
}

#[test]
fn parse_custom_type_typed_string() {
assert_eq!(
pg().verified_expr("public.mood 'happy'"),
Expr::TypedString(TypedString {
data_type: DataType::Custom(
ObjectName::from(vec!["public".into(), "mood".into()]),
vec![]
),
value: Value::SingleQuotedString("happy".to_string()).with_empty_span(),
uses_odbc_syntax: false,
})
);
pg().verified_stmt("SELECT mood 'happy'");
pg().verified_stmt("SELECT count(*) FROM t WHERE m = mood 'sad'");
pg().verified_stmt("SELECT pg_typeof(mood 'happy')");
pg().verified_stmt(r#"SELECT "mood" 'happy'"#);
pg().verified_stmt("SELECT status 'on'");
pg().verified_stmt("SELECT left 'x'");
pg().verified_stmt("SELECT pg_catalog.coalesce 'x'");
pg().verified_stmt("SELECT mood E'happy'");
pg().verified_stmt("SELECT mood $$happy$$");

assert!(matches!(
pg().verified_expr("NOT 'a' LIKE 'b'"),
Expr::UnaryOp {
op: UnaryOperator::Not,
..
}
));
assert!(matches!(
pg().verified_expr("point '1,2'"),
Expr::TypedString(TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::Point),
..
})
));
assert_eq!(
pg().parse_sql_statements("SELECT pg_typeof(coalesce 'x')")
.unwrap_err(),
ParserError::ParserError("Expected: ), found: 'x'".to_string())
);
assert_eq!(
pg().parse_sql_statements("SELECT mood 5").unwrap_err(),
ParserError::ParserError("Expected: end of statement, found: 5".to_string())
);
}
Loading