From 2a2abc8dad933068e2b024eb1ed986f2c141e05a Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Wed, 26 Nov 2025 10:51:11 +0100 Subject: [PATCH 1/3] Added support for `DROP OPERATOR` syntax (#2102) Co-authored-by: Ifeanyi Ubah --- src/ast/ddl.rs | 59 +++++++++++++++++++++++++ src/ast/mod.rs | 24 ++++++---- src/ast/spans.rs | 1 + src/parser/mod.rs | 44 ++++++++++++++++++- tests/sqlparser_postgres.rs | 87 +++++++++++++++++++++++++++++++++++++ 5 files changed, 206 insertions(+), 9 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 3a38fbaa..6f5010f0 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -4188,3 +4188,62 @@ impl fmt::Display for OperatorPurpose { } } } + +/// `DROP OPERATOR` statement +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DropOperator { + /// `IF EXISTS` clause + pub if_exists: bool, + /// One or more operators to drop with their signatures + pub operators: Vec, + /// `CASCADE or RESTRICT` + pub drop_behavior: Option, +} + +/// Operator signature for a `DROP OPERATOR` statement +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DropOperatorSignature { + /// Operator name + pub name: ObjectName, + /// Left operand type + pub left_type: Option, + /// Right operand type + pub right_type: DataType, +} + +impl fmt::Display for DropOperatorSignature { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} (", self.name)?; + if let Some(left_type) = &self.left_type { + write!(f, "{}", left_type)?; + } else { + write!(f, "NONE")?; + } + write!(f, ", {})", self.right_type) + } +} + +impl fmt::Display for DropOperator { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "DROP OPERATOR")?; + if self.if_exists { + write!(f, " IF EXISTS")?; + } + write!(f, " {}", display_comma_separated(&self.operators))?; + if let Some(drop_behavior) = &self.drop_behavior { + write!(f, " {}", drop_behavior)?; + } + Ok(()) + } +} + +impl Spanned for DropOperator { + fn span(&self) -> Span { + Span::empty() + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2c452a69..8b50452c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -67,14 +67,15 @@ pub use self::ddl::{ ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, CreateExtension, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreateTable, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, - DropBehavior, DropExtension, DropFunction, DropTrigger, GeneratedAs, GeneratedExpressionMode, - IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, - IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, - NullsDistinctOption, OperatorArgTypes, OperatorClassItem, OperatorPurpose, Owner, Partition, - ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, TagsColumnOption, - TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, - UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, - UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, + DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorSignature, DropTrigger, + GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, + IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, + IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, + OperatorClassItem, OperatorPurpose, Owner, Partition, ProcedureParam, ReferentialAction, + RenameTableNameKind, ReplicaIdentity, TagsColumnOption, TriggerObjectKind, Truncate, + UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, + UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, + UserDefinedTypeStorage, ViewColumnDef, }; pub use self::dml::{Delete, Insert, Update}; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -3573,6 +3574,12 @@ pub enum Statement { /// DropExtension(DropExtension), /// ```sql + /// DROP OPERATOR [ IF EXISTS ] name ( { left_type | NONE } , right_type ) [, ...] [ CASCADE | RESTRICT ] + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + DropOperator(DropOperator), + /// ```sql /// FETCH /// ``` /// Retrieve rows from a query using a cursor @@ -4836,6 +4843,7 @@ impl fmt::Display for Statement { Statement::CreateIndex(create_index) => create_index.fmt(f), Statement::CreateExtension(create_extension) => write!(f, "{create_extension}"), Statement::DropExtension(drop_extension) => write!(f, "{drop_extension}"), + Statement::DropOperator(drop_operator) => write!(f, "{drop_operator}"), Statement::CreateRole(create_role) => write!(f, "{create_role}"), Statement::CreateSecret { or_replace, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index b50cc56d..66799fe5 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -375,6 +375,7 @@ impl Spanned for Statement { Statement::CreateRole(create_role) => create_role.span(), Statement::CreateExtension(create_extension) => create_extension.span(), Statement::DropExtension(drop_extension) => drop_extension.span(), + Statement::DropOperator(drop_operator) => drop_operator.span(), Statement::CreateSecret { .. } => Span::empty(), Statement::CreateServer { .. } => Span::empty(), Statement::CreateConnector { .. } => Span::empty(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 57d94bee..759e2eef 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6767,9 +6767,11 @@ impl<'a> Parser<'a> { return self.parse_drop_trigger(); } else if self.parse_keyword(Keyword::EXTENSION) { return self.parse_drop_extension(); + } else if self.parse_keyword(Keyword::OPERATOR) { + return self.parse_drop_operator(); } else { return self.expected( - "CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW or USER after DROP", + "CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW or USER after DROP", self.peek_token(), ); }; @@ -7525,6 +7527,46 @@ impl<'a> Parser<'a> { })) } + /// Parse a[Statement::DropOperator] statement. + /// + pub fn parse_drop_operator(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let operators = self.parse_comma_separated(|p| p.parse_drop_operator_signature())?; + let drop_behavior = self.parse_optional_drop_behavior(); + Ok(Statement::DropOperator(DropOperator { + if_exists, + operators, + drop_behavior, + })) + } + + /// Parse an operator signature for a [Statement::DropOperator] + /// Format: `name ( { left_type | NONE } , right_type )` + fn parse_drop_operator_signature(&mut self) -> Result { + let name = self.parse_operator_name()?; + self.expect_token(&Token::LParen)?; + + // Parse left operand type (or NONE for prefix operators) + let left_type = if self.parse_keyword(Keyword::NONE) { + None + } else { + Some(self.parse_data_type()?) + }; + + self.expect_token(&Token::Comma)?; + + // Parse right operand type (always required) + let right_type = self.parse_data_type()?; + + self.expect_token(&Token::RParen)?; + + Ok(DropOperatorSignature { + name, + left_type, + right_type, + }) + } + //TODO: Implement parsing for Skewed pub fn parse_hive_distribution(&mut self) -> Result { if self.parse_keywords(&[Keyword::PARTITIONED, Keyword::BY]) { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index a14ff5ec..858e2e80 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -23,6 +23,7 @@ mod test_utils; use helpers::attached_token::AttachedToken; +use sqlparser::ast::{DataType, DropBehavior, DropOperator, DropOperatorSignature}; use sqlparser::tokenizer::Span; use test_utils::*; @@ -6763,6 +6764,92 @@ fn parse_create_operator() { assert!(pg().parse_sql_statements("CREATE OPERATOR > ())").is_err()); } +#[test] +fn parse_drop_operator() { + // Test DROP OPERATOR with NONE for prefix operator + let sql = "DROP OPERATOR ~ (NONE, BIT)"; + assert_eq!( + pg_and_generic().verified_stmt(sql), + Statement::DropOperator(DropOperator { + if_exists: false, + operators: vec![DropOperatorSignature { + name: ObjectName::from(vec![Ident::new("~")]), + left_type: None, + right_type: DataType::Bit(None), + }], + drop_behavior: None, + }) + ); + + for if_exist in [true, false] { + for cascading in [ + None, + Some(DropBehavior::Cascade), + Some(DropBehavior::Restrict), + ] { + for op in &["<", ">", "<=", ">=", "<>", "||", "&&", "<<", ">>"] { + let sql = format!( + "DROP OPERATOR{} {op} (INTEGER, INTEGER){}", + if if_exist { " IF EXISTS" } else { "" }, + match cascading { + Some(cascading) => format!(" {cascading}"), + None => String::new(), + } + ); + assert_eq!( + pg_and_generic().verified_stmt(&sql), + Statement::DropOperator(DropOperator { + if_exists: if_exist, + operators: vec![DropOperatorSignature { + name: ObjectName::from(vec![Ident::new(*op)]), + left_type: Some(DataType::Integer(None)), + right_type: DataType::Integer(None), + }], + drop_behavior: cascading, + }) + ); + } + } + } + + // Test DROP OPERATOR with schema-qualified operator name + let sql = "DROP OPERATOR myschema.@@ (TEXT, TEXT)"; + assert_eq!( + pg_and_generic().verified_stmt(sql), + Statement::DropOperator(DropOperator { + if_exists: false, + operators: vec![DropOperatorSignature { + name: ObjectName::from(vec![Ident::new("myschema"), Ident::new("@@")]), + left_type: Some(DataType::Text), + right_type: DataType::Text, + }], + drop_behavior: None, + }) + ); + + // Test DROP OPERATOR with multiple operators, IF EXISTS and CASCADE + let sql = "DROP OPERATOR IF EXISTS + (INTEGER, INTEGER), - (INTEGER, INTEGER) CASCADE"; + assert_eq!( + pg_and_generic().verified_stmt(sql), + Statement::DropOperator(DropOperator { + if_exists: true, + operators: vec![ + DropOperatorSignature { + name: ObjectName::from(vec![Ident::new("+")]), + left_type: Some(DataType::Integer(None)), + right_type: DataType::Integer(None), + }, + DropOperatorSignature { + name: ObjectName::from(vec![Ident::new("-")]), + left_type: Some(DataType::Integer(None)), + right_type: DataType::Integer(None), + } + ], + drop_behavior: Some(DropBehavior::Cascade), + }) + ); +} + #[test] fn parse_create_operator_family() { for index_method in &["btree", "hash", "gist", "gin", "spgist", "brin"] { From e380494eb0440a31d83542a99d9e9df8de3923ca Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Wed, 26 Nov 2025 01:52:23 -0800 Subject: [PATCH 2/3] Only set `hive_formats` on `CreateTable` if formats are present (#2105) --- src/dialect/snowflake.rs | 10 ++++----- src/parser/mod.rs | 45 +++++++++++++++++++++++-------------- tests/sqlparser_common.rs | 11 +++++++++ tests/sqlparser_duckdb.rs | 7 +----- tests/sqlparser_mssql.rs | 14 ++---------- tests/sqlparser_postgres.rs | 7 +----- 6 files changed, 48 insertions(+), 46 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index bb0d4f16..4cfaddce 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -393,7 +393,7 @@ impl Dialect for SnowflakeDialect { fn is_column_alias(&self, kw: &Keyword, parser: &mut Parser) -> bool { match kw { - // The following keywords can be considered an alias as long as + // The following keywords can be considered an alias as long as // they are not followed by other tokens that may change their meaning // e.g. `SELECT * EXCEPT (col1) FROM tbl` Keyword::EXCEPT @@ -408,7 +408,7 @@ impl Dialect for SnowflakeDialect { Keyword::LIMIT | Keyword::OFFSET if peek_for_limit_options(parser) => false, // `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT` - // which would give it a different meanings, for example: + // which would give it a different meanings, for example: // `SELECT 1 FETCH FIRST 10 ROWS` - not an alias // `SELECT 1 FETCH 10` - not an alias Keyword::FETCH if parser.peek_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]).is_some() @@ -417,8 +417,8 @@ impl Dialect for SnowflakeDialect { false } - // Reserved keywords by the Snowflake dialect, which seem to be less strictive - // than what is listed in `keywords::RESERVED_FOR_COLUMN_ALIAS`. The following + // Reserved keywords by the Snowflake dialect, which seem to be less strictive + // than what is listed in `keywords::RESERVED_FOR_COLUMN_ALIAS`. The following // keywords were tested with the this statement: `SELECT 1 `. Keyword::FROM | Keyword::GROUP @@ -688,7 +688,7 @@ pub fn parse_create_table( .iceberg(iceberg) .global(global) .dynamic(dynamic) - .hive_formats(Some(Default::default())); + .hive_formats(None); // Snowflake does not enforce order of the parameters in the statement. The parser needs to // parse the statement in a loop. diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 759e2eef..759f5189 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5831,15 +5831,19 @@ impl<'a> Parser<'a> { let hive_distribution = self.parse_hive_distribution()?; let hive_formats = self.parse_hive_formats()?; - let file_format = if let Some(ff) = &hive_formats.storage { - match ff { - HiveIOFormat::FileFormat { format } => Some(*format), - _ => None, + let file_format = if let Some(ref hf) = hive_formats { + if let Some(ref ff) = hf.storage { + match ff { + HiveIOFormat::FileFormat { format } => Some(*format), + _ => None, + } + } else { + None } } else { None }; - let location = hive_formats.location.clone(); + let location = hive_formats.as_ref().and_then(|hf| hf.location.clone()); let table_properties = self.parse_options(Keyword::TBLPROPERTIES)?; let table_options = if !table_properties.is_empty() { CreateTableOptions::TableProperties(table_properties) @@ -5850,7 +5854,7 @@ impl<'a> Parser<'a> { .columns(columns) .constraints(constraints) .hive_distribution(hive_distribution) - .hive_formats(Some(hive_formats)) + .hive_formats(hive_formats) .table_options(table_options) .or_replace(or_replace) .if_not_exists(if_not_exists) @@ -7579,8 +7583,8 @@ impl<'a> Parser<'a> { } } - pub fn parse_hive_formats(&mut self) -> Result { - let mut hive_format = HiveFormat::default(); + pub fn parse_hive_formats(&mut self) -> Result, ParserError> { + let mut hive_format: Option = None; loop { match self.parse_one_of_keywords(&[ Keyword::ROW, @@ -7589,7 +7593,9 @@ impl<'a> Parser<'a> { Keyword::WITH, ]) { Some(Keyword::ROW) => { - hive_format.row_format = Some(self.parse_row_format()?); + hive_format + .get_or_insert_with(HiveFormat::default) + .row_format = Some(self.parse_row_format()?); } Some(Keyword::STORED) => { self.expect_keyword_is(Keyword::AS)?; @@ -7597,24 +7603,29 @@ impl<'a> Parser<'a> { let input_format = self.parse_expr()?; self.expect_keyword_is(Keyword::OUTPUTFORMAT)?; let output_format = self.parse_expr()?; - hive_format.storage = Some(HiveIOFormat::IOF { - input_format, - output_format, - }); + hive_format.get_or_insert_with(HiveFormat::default).storage = + Some(HiveIOFormat::IOF { + input_format, + output_format, + }); } else { let format = self.parse_file_format()?; - hive_format.storage = Some(HiveIOFormat::FileFormat { format }); + hive_format.get_or_insert_with(HiveFormat::default).storage = + Some(HiveIOFormat::FileFormat { format }); } } Some(Keyword::LOCATION) => { - hive_format.location = Some(self.parse_literal_string()?); + hive_format.get_or_insert_with(HiveFormat::default).location = + Some(self.parse_literal_string()?); } Some(Keyword::WITH) => { self.prev_token(); let properties = self .parse_options_with_keywords(&[Keyword::WITH, Keyword::SERDEPROPERTIES])?; if !properties.is_empty() { - hive_format.serde_properties = Some(properties); + hive_format + .get_or_insert_with(HiveFormat::default) + .serde_properties = Some(properties); } else { break; } @@ -7829,7 +7840,7 @@ impl<'a> Parser<'a> { .if_not_exists(if_not_exists) .transient(transient) .hive_distribution(hive_distribution) - .hive_formats(Some(hive_formats)) + .hive_formats(hive_formats) .global(global) .query(query) .without_rowid(without_rowid) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3649e8d3..91952b8c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4724,6 +4724,17 @@ fn parse_create_external_table_lowercase() { assert_matches!(ast, Statement::CreateTable(CreateTable { .. })); } +#[test] +fn parse_create_table_hive_formats_none_when_no_options() { + let sql = "CREATE TABLE simple_table (id INT, name VARCHAR(100))"; + match verified_stmt(sql) { + Statement::CreateTable(CreateTable { hive_formats, .. }) => { + assert_eq!(hive_formats, None); + } + _ => unreachable!(), + } +} + #[test] fn parse_alter_table() { let add_column = "ALTER TABLE tab ADD COLUMN foo TEXT;"; diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 0f805195..73a1afe2 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -739,12 +739,7 @@ fn test_duckdb_union_datatype() { ], constraints: Default::default(), hive_distribution: HiveDistributionStyle::NONE, - hive_formats: Some(HiveFormat { - row_format: Default::default(), - serde_properties: Default::default(), - storage: Default::default(), - location: Default::default() - }), + hive_formats: None, file_format: Default::default(), location: Default::default(), query: Default::default(), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 99a298f8..37e8e962 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1881,12 +1881,7 @@ fn parse_create_table_with_valid_options() { ], constraints: vec![], hive_distribution: HiveDistributionStyle::NONE, - hive_formats: Some(HiveFormat { - row_format: None, - serde_properties: None, - storage: None, - location: None, - },), + hive_formats: None, file_format: None, location: None, query: None, @@ -2053,12 +2048,7 @@ fn parse_create_table_with_identity_column() { },], constraints: vec![], hive_distribution: HiveDistributionStyle::NONE, - hive_formats: Some(HiveFormat { - row_format: None, - serde_properties: None, - storage: None, - location: None, - },), + hive_formats: None, file_format: None, location: None, query: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 858e2e80..7309a6ba 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6016,12 +6016,7 @@ fn parse_trigger_related_functions() { ], constraints: vec![], hive_distribution: HiveDistributionStyle::NONE, - hive_formats: Some(HiveFormat { - row_format: None, - serde_properties: None, - storage: None, - location: None - }), + hive_formats: None, file_format: None, location: None, query: None, From 4beea9a4bccb867b34f466f0ad1793ebcdf370ba Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Wed, 26 Nov 2025 11:21:57 +0100 Subject: [PATCH 3/3] Support PostgreSQL C Functions with Multiple AS Parameters (#2095) Co-authored-by: Ifeanyi Ubah --- src/ast/ddl.rs | 8 ++- src/ast/mod.rs | 15 ++++- src/parser/mod.rs | 48 +++++++++------ tests/sqlparser_hive.rs | 11 ++-- tests/sqlparser_postgres.rs | 115 ++++++++++++++++++++++++++++-------- 5 files changed, 147 insertions(+), 50 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 6f5010f0..078e3623 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3099,8 +3099,12 @@ impl fmt::Display for CreateFunction { if let Some(remote_connection) = &self.remote_connection { write!(f, " REMOTE WITH CONNECTION {remote_connection}")?; } - if let Some(CreateFunctionBody::AsBeforeOptions(function_body)) = &self.function_body { - write!(f, " AS {function_body}")?; + if let Some(CreateFunctionBody::AsBeforeOptions { body, link_symbol }) = &self.function_body + { + write!(f, " AS {body}")?; + if let Some(link_symbol) = link_symbol { + write!(f, ", {link_symbol}")?; + } } if let Some(CreateFunctionBody::Return(function_body)) = &self.function_body { write!(f, " RETURN {function_body}")?; diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8b50452c..2d768c24 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9108,7 +9108,20 @@ pub enum CreateFunctionBody { /// ``` /// /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 - AsBeforeOptions(Expr), + /// [PostgreSQL]: https://www.postgresql.org/docs/current/sql-createfunction.html + AsBeforeOptions { + /// The primary expression. + body: Expr, + /// Link symbol if the primary expression contains the name of shared library file. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION cas_in(input cstring) RETURNS cas + /// AS 'MODULE_PATHNAME', 'cas_in_wrapper' + /// ``` + /// [PostgreSQL]: https://www.postgresql.org/docs/current/sql-createfunction.html + link_symbol: Option, + }, /// A function body expression using the 'AS' keyword and shows up /// after any `OPTIONS` clause. /// diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 759f5189..0d561089 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5204,9 +5204,7 @@ impl<'a> Parser<'a> { } if self.parse_keyword(Keyword::AS) { ensure_not_set(&body.function_body, "AS")?; - body.function_body = Some(CreateFunctionBody::AsBeforeOptions( - self.parse_create_function_body_string()?, - )); + body.function_body = Some(self.parse_create_function_body_string()?); } else if self.parse_keyword(Keyword::LANGUAGE) { ensure_not_set(&body.language, "LANGUAGE")?; body.language = Some(self.parse_identifier()?); @@ -5298,7 +5296,7 @@ impl<'a> Parser<'a> { let name = self.parse_object_name(false)?; self.expect_keyword_is(Keyword::AS)?; - let as_ = self.parse_create_function_body_string()?; + let body = self.parse_create_function_body_string()?; let using = self.parse_optional_create_function_using()?; Ok(Statement::CreateFunction(CreateFunction { @@ -5306,7 +5304,7 @@ impl<'a> Parser<'a> { or_replace, temporary, name, - function_body: Some(CreateFunctionBody::AsBeforeOptions(as_)), + function_body: Some(body), using, if_not_exists: false, args: None, @@ -5368,7 +5366,10 @@ impl<'a> Parser<'a> { let expr = self.parse_expr()?; if options.is_none() { options = self.maybe_parse_options(Keyword::OPTIONS)?; - Some(CreateFunctionBody::AsBeforeOptions(expr)) + Some(CreateFunctionBody::AsBeforeOptions { + body: expr, + link_symbol: None, + }) } else { Some(CreateFunctionBody::AsAfterOptions(expr)) } @@ -10574,19 +10575,30 @@ impl<'a> Parser<'a> { /// Parse the body of a `CREATE FUNCTION` specified as a string. /// e.g. `CREATE FUNCTION ... AS $$ body $$`. - fn parse_create_function_body_string(&mut self) -> Result { - let peek_token = self.peek_token(); - let span = peek_token.span; - match peek_token.token { - Token::DollarQuotedString(s) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => - { - self.next_token(); - Ok(Expr::Value(Value::DollarQuotedString(s).with_span(span))) + fn parse_create_function_body_string(&mut self) -> Result { + let parse_string_expr = |parser: &mut Parser| -> Result { + let peek_token = parser.peek_token(); + let span = peek_token.span; + match peek_token.token { + Token::DollarQuotedString(s) if dialect_of!(parser is PostgreSqlDialect | GenericDialect) => + { + parser.next_token(); + Ok(Expr::Value(Value::DollarQuotedString(s).with_span(span))) + } + _ => Ok(Expr::Value( + Value::SingleQuotedString(parser.parse_literal_string()?).with_span(span), + )), } - _ => Ok(Expr::Value( - Value::SingleQuotedString(self.parse_literal_string()?).with_span(span), - )), - } + }; + + Ok(CreateFunctionBody::AsBeforeOptions { + body: parse_string_expr(self)?, + link_symbol: if self.consume_token(&Token::Comma) { + Some(parse_string_expr(self)?) + } else { + None + }, + }) } /// Parse a literal string diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 56a72ec8..386bab7f 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -408,10 +408,13 @@ fn parse_create_function() { assert_eq!(name.to_string(), "mydb.myfunc"); assert_eq!( function_body, - Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - (Value::SingleQuotedString("org.random.class.Name".to_string())) - .with_empty_span() - ))) + Some(CreateFunctionBody::AsBeforeOptions { + body: Expr::Value( + (Value::SingleQuotedString("org.random.class.Name".to_string())) + .with_empty_span() + ), + link_symbol: None, + }) ); assert_eq!( using, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7309a6ba..91150666 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4260,9 +4260,12 @@ $$"#; behavior: None, called_on_null: None, parallel: None, - function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF str1 <> str2 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() - ))), + function_body: Some(CreateFunctionBody::AsBeforeOptions { + body: Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF str1 <> str2 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ), + link_symbol: None, + }), if_not_exists: false, using: None, determinism_specifier: None, @@ -4298,9 +4301,12 @@ $$"#; behavior: None, called_on_null: None, parallel: None, - function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF int1 <> 0 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() - ))), + function_body: Some(CreateFunctionBody::AsBeforeOptions { + body: Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF int1 <> 0 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ), + link_symbol: None, + }), if_not_exists: false, using: None, determinism_specifier: None, @@ -4340,9 +4346,12 @@ $$"#; behavior: None, called_on_null: None, parallel: None, - function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF a <> b THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() - ))), + function_body: Some(CreateFunctionBody::AsBeforeOptions { + body: Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF a <> b THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ), + link_symbol: None, + }), if_not_exists: false, using: None, determinism_specifier: None, @@ -4382,9 +4391,12 @@ $$"#; behavior: None, called_on_null: None, parallel: None, - function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF int1 <> int2 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() - ))), + function_body: Some(CreateFunctionBody::AsBeforeOptions { + body: Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF int1 <> int2 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ), + link_symbol: None, + }), if_not_exists: false, using: None, determinism_specifier: None, @@ -4417,13 +4429,16 @@ $$"#; behavior: None, called_on_null: None, parallel: None, - function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - (Value::DollarQuotedString(DollarQuotedString { - value: "\n BEGIN\n RETURN TRUE;\n END;\n ".to_owned(), - tag: None - })) - .with_empty_span() - ))), + function_body: Some(CreateFunctionBody::AsBeforeOptions { + body: Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + value: "\n BEGIN\n RETURN TRUE;\n END;\n ".to_owned(), + tag: None + })) + .with_empty_span() + ), + link_symbol: None, + }), if_not_exists: false, using: None, determinism_specifier: None, @@ -4455,9 +4470,12 @@ fn parse_create_function() { behavior: Some(FunctionBehavior::Immutable), called_on_null: Some(FunctionCalledOnNull::Strict), parallel: Some(FunctionParallel::Safe), - function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - (Value::SingleQuotedString("select $1 + $2;".into())).with_empty_span() - ))), + function_body: Some(CreateFunctionBody::AsBeforeOptions { + body: Expr::Value( + (Value::SingleQuotedString("select $1 + $2;".into())).with_empty_span() + ), + link_symbol: None, + }), if_not_exists: false, using: None, determinism_specifier: None, @@ -4488,6 +4506,52 @@ fn parse_incorrect_create_function_parallel() { assert!(pg().parse_sql_statements(sql).is_err()); } +#[test] +fn parse_create_function_c_with_module_pathname() { + let sql = "CREATE FUNCTION cas_in(input cstring) RETURNS cas LANGUAGE c IMMUTABLE PARALLEL SAFE AS 'MODULE_PATHNAME', 'cas_in_wrapper'"; + assert_eq!( + pg_and_generic().verified_stmt(sql), + Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: false, + temporary: false, + name: ObjectName::from(vec![Ident::new("cas_in")]), + args: Some(vec![OperateFunctionArg::with_name( + "input", + DataType::Custom(ObjectName::from(vec![Ident::new("cstring")]), vec![]), + ),]), + return_type: Some(DataType::Custom( + ObjectName::from(vec![Ident::new("cas")]), + vec![] + )), + language: Some("c".into()), + behavior: Some(FunctionBehavior::Immutable), + called_on_null: None, + parallel: Some(FunctionParallel::Safe), + function_body: Some(CreateFunctionBody::AsBeforeOptions { + body: Expr::Value( + (Value::SingleQuotedString("MODULE_PATHNAME".into())).with_empty_span() + ), + link_symbol: Some(Expr::Value( + (Value::SingleQuotedString("cas_in_wrapper".into())).with_empty_span() + )), + }), + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + ); + + // Test that attribute order flexibility works (IMMUTABLE before LANGUAGE) + let sql_alt_order = "CREATE FUNCTION cas_in(input cstring) RETURNS cas IMMUTABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'cas_in_wrapper'"; + pg_and_generic().one_statement_parses_to( + sql_alt_order, + "CREATE FUNCTION cas_in(input cstring) RETURNS cas LANGUAGE c IMMUTABLE PARALLEL SAFE AS 'MODULE_PATHNAME', 'cas_in_wrapper'" + ); +} + #[test] fn parse_drop_function() { let sql = "DROP FUNCTION IF EXISTS test_func"; @@ -6070,8 +6134,8 @@ fn parse_trigger_related_functions() { args: Some(vec![]), return_type: Some(DataType::Trigger), function_body: Some( - CreateFunctionBody::AsBeforeOptions( - Expr::Value(( + CreateFunctionBody::AsBeforeOptions { + body: Expr::Value(( Value::DollarQuotedString( DollarQuotedString { value: "\n BEGIN\n -- Check that empname and salary are given\n IF NEW.empname IS NULL THEN\n RAISE EXCEPTION 'empname cannot be null';\n END IF;\n IF NEW.salary IS NULL THEN\n RAISE EXCEPTION '% cannot have null salary', NEW.empname;\n END IF;\n\n -- Who works for us when they must pay for it?\n IF NEW.salary < 0 THEN\n RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;\n END IF;\n\n -- Remember who changed the payroll when\n NEW.last_date := current_timestamp;\n NEW.last_user := current_user;\n RETURN NEW;\n END;\n ".to_owned(), @@ -6081,7 +6145,8 @@ fn parse_trigger_related_functions() { }, ) ).with_empty_span()), - ), + link_symbol: None, + }, ), behavior: None, called_on_null: None,