diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 3a38fbaa..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}")?; @@ -4188,3 +4192,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 d0c7e834..a4adb7d5 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::{Copy, Delete, Insert, Update}; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -3560,6 +3561,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 @@ -4786,6 +4793,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, @@ -9050,7 +9058,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/ast/spans.rs b/src/ast/spans.rs index fbf9a763..d142c41e 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -368,6 +368,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/dialect/snowflake.rs b/src/dialect/snowflake.rs index bf5d3e16..cdbe876d 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -397,7 +397,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 @@ -412,7 +412,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() @@ -421,8 +421,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 @@ -692,7 +692,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 ad332231..d470103f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5140,9 +5140,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()?); @@ -5234,7 +5232,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 { @@ -5242,7 +5240,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, @@ -5304,7 +5302,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)) } @@ -5767,15 +5768,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) @@ -5786,7 +5791,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) @@ -6703,9 +6708,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(), ); }; @@ -7461,6 +7468,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]) { @@ -7473,8 +7520,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, @@ -7483,7 +7530,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)?; @@ -7491,24 +7540,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; } @@ -7723,7 +7777,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) @@ -10423,19 +10477,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_common.rs b/tests/sqlparser_common.rs index c94b154a..a0cf2791 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_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_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 100e551d..da008a92 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::*; @@ -4281,9 +4282,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, @@ -4319,9 +4323,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, @@ -4361,9 +4368,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, @@ -4403,9 +4413,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, @@ -4438,13 +4451,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, @@ -4476,9 +4492,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, @@ -4509,6 +4528,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"; @@ -6037,12 +6102,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, @@ -6096,8 +6156,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(), @@ -6107,7 +6167,8 @@ fn parse_trigger_related_functions() { }, ) ).with_empty_span()), - ), + link_symbol: None, + }, ), behavior: None, called_on_null: None, @@ -6785,6 +6846,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"] {