diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c9de747c7..1227ce935 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2454,14 +2454,64 @@ pub enum Statement { /// Supported variants: /// 1. [Hive](https://cwiki.apache.org/confluence/display/hive/languagemanual+ddl#LanguageManualDDL-Create/Drop/ReloadFunction) /// 2. [Postgres](https://www.postgresql.org/docs/15/sql-createfunction.html) + /// 3. [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_function_statement) CreateFunction { or_replace: bool, temporary: bool, + if_not_exists: bool, name: ObjectName, args: Option>, return_type: Option, - /// Optional parameters. - params: CreateFunctionBody, + /// The expression that defines the function. + /// + /// Examples: + /// ```sql + /// AS ((SELECT 1)) + /// AS "console.log();" + /// ``` + function_body: Option, + /// Behavior attribute for the function + /// + /// IMMUTABLE | STABLE | VOLATILE + /// + /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) + behavior: Option, + /// CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT + /// + /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) + called_on_null: Option, + /// PARALLEL { UNSAFE | RESTRICTED | SAFE } + /// + /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) + parallel: Option, + /// USING ... (Hive only) + using: Option, + /// Language used in a UDF definition. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION foo() LANGUAGE js AS "console.log();" + /// ``` + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_a_javascript_udf) + language: Option, + /// Determinism keyword used for non-sql UDF definitions. + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11) + determinism_specifier: Option, + /// List of options for creating the function. + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11) + options: Option>, + /// Connection resource for a remote function. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION foo() + /// RETURNS FLOAT64 + /// REMOTE WITH CONNECTION us.myconnection + /// ``` + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_a_remote_function) + remote_connection: Option, }, /// ```sql /// CREATE PROCEDURE @@ -3152,16 +3202,26 @@ impl fmt::Display for Statement { Statement::CreateFunction { or_replace, temporary, + if_not_exists, name, args, return_type, - params, + function_body, + language, + behavior, + called_on_null, + parallel, + using, + determinism_specifier, + options, + remote_connection, } => { write!( f, - "CREATE {or_replace}{temp}FUNCTION {name}", + "CREATE {or_replace}{temp}FUNCTION {if_not_exists}{name}", temp = if *temporary { "TEMPORARY " } else { "" }, or_replace = if *or_replace { "OR REPLACE " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, )?; if let Some(args) = args { write!(f, "({})", display_comma_separated(args))?; @@ -3169,7 +3229,43 @@ impl fmt::Display for Statement { if let Some(return_type) = return_type { write!(f, " RETURNS {return_type}")?; } - write!(f, "{params}")?; + if let Some(determinism_specifier) = determinism_specifier { + write!(f, " {determinism_specifier}")?; + } + if let Some(language) = language { + write!(f, " LANGUAGE {language}")?; + } + if let Some(behavior) = behavior { + write!(f, " {behavior}")?; + } + if let Some(called_on_null) = called_on_null { + write!(f, " {called_on_null}")?; + } + if let Some(parallel) = parallel { + write!(f, " {parallel}")?; + } + if let Some(remote_connection) = remote_connection { + write!(f, " REMOTE WITH CONNECTION {remote_connection}")?; + } + if let Some(CreateFunctionBody::AsBeforeOptions(function_body)) = function_body { + write!(f, " AS {function_body}")?; + } + if let Some(CreateFunctionBody::Return(function_body)) = function_body { + write!(f, " RETURN {function_body}")?; + } + if let Some(using) = using { + write!(f, " {using}")?; + } + if let Some(options) = options { + write!( + f, + " OPTIONS({})", + display_comma_separated(options.as_slice()) + )?; + } + if let Some(CreateFunctionBody::AsAfterOptions(function_body)) = function_body { + write!(f, " AS {function_body}")?; + } Ok(()) } Statement::CreateProcedure { @@ -6143,75 +6239,74 @@ impl fmt::Display for FunctionParallel { } } +/// [BigQuery] Determinism specifier used in a UDF definition. +/// +/// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum FunctionDefinition { - SingleQuotedDef(String), - DoubleDollarDef(String), +pub enum FunctionDeterminismSpecifier { + Deterministic, + NotDeterministic, } -impl fmt::Display for FunctionDefinition { +impl fmt::Display for FunctionDeterminismSpecifier { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - FunctionDefinition::SingleQuotedDef(s) => write!(f, "'{s}'")?, - FunctionDefinition::DoubleDollarDef(s) => write!(f, "$${s}$$")?, + FunctionDeterminismSpecifier::Deterministic => { + write!(f, "DETERMINISTIC") + } + FunctionDeterminismSpecifier::NotDeterministic => { + write!(f, "NOT DETERMINISTIC") + } } - Ok(()) } } -/// Postgres specific feature. +/// Represent the expression body of a `CREATE FUNCTION` statement as well as +/// where within the statement, the body shows up. /// -/// See [Postgres docs](https://www.postgresql.org/docs/15/sql-createfunction.html) -/// for more details -#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +/// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 +/// [Postgres]: https://www.postgresql.org/docs/15/sql-createfunction.html +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct CreateFunctionBody { - /// LANGUAGE lang_name - pub language: Option, - /// IMMUTABLE | STABLE | VOLATILE - pub behavior: Option, - /// CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT - pub called_on_null: Option, - /// PARALLEL { UNSAFE | RESTRICTED | SAFE } - pub parallel: Option, - /// AS 'definition' +pub enum CreateFunctionBody { + /// A function body expression using the 'AS' keyword and shows up + /// before any `OPTIONS` clause. /// - /// Note that Hive's `AS class_name` is also parsed here. - pub as_: Option, - /// RETURN expression - pub return_: Option, - /// USING ... (Hive only) - pub using: Option, -} - -impl fmt::Display for CreateFunctionBody { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(language) = &self.language { - write!(f, " LANGUAGE {language}")?; - } - if let Some(behavior) = &self.behavior { - write!(f, " {behavior}")?; - } - if let Some(called_on_null) = &self.called_on_null { - write!(f, " {called_on_null}")?; - } - if let Some(parallel) = &self.parallel { - write!(f, " {parallel}")?; - } - if let Some(definition) = &self.as_ { - write!(f, " AS {definition}")?; - } - if let Some(expr) = &self.return_ { - write!(f, " RETURN {expr}")?; - } - if let Some(using) = &self.using { - write!(f, " {using}")?; - } - Ok(()) - } + /// Example: + /// ```sql + /// CREATE FUNCTION myfunc(x FLOAT64, y FLOAT64) RETURNS FLOAT64 + /// AS (x * y) + /// OPTIONS(description="desc"); + /// ``` + /// + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 + AsBeforeOptions(Expr), + /// A function body expression using the 'AS' keyword and shows up + /// after any `OPTIONS` clause. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION myfunc(x FLOAT64, y FLOAT64) RETURNS FLOAT64 + /// OPTIONS(description="desc") + /// AS (x * y); + /// ``` + /// + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 + AsAfterOptions(Expr), + /// Function body expression using the 'RETURN' keyword. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION myfunc(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER + /// LANGUAGE SQL + /// RETURN a + b; + /// ``` + /// + /// [Postgres]: https://www.postgresql.org/docs/current/sql-createfunction.html + Return(Expr), } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/keywords.rs b/src/keywords.rs index e67fffd97..06086297c 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -572,6 +572,7 @@ define_keywords!( RELATIVE, RELAY, RELEASE, + REMOTE, RENAME, REORG, REPAIR, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f88aefd10..f88f2c189 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3590,95 +3590,53 @@ impl<'a> Parser<'a> { temporary: bool, ) -> Result { if dialect_of!(self is HiveDialect) { - let name = self.parse_object_name(false)?; - self.expect_keyword(Keyword::AS)?; - let class_name = self.parse_function_definition()?; - let params = CreateFunctionBody { - as_: Some(class_name), - using: self.parse_optional_create_function_using()?, - ..Default::default() - }; - - Ok(Statement::CreateFunction { - or_replace, - temporary, - name, - args: None, - return_type: None, - params, - }) + self.parse_hive_create_function(or_replace, temporary) } else if dialect_of!(self is PostgreSqlDialect | GenericDialect) { - let name = self.parse_object_name(false)?; - self.expect_token(&Token::LParen)?; - let args = if self.consume_token(&Token::RParen) { - self.prev_token(); - None - } else { - Some(self.parse_comma_separated(Parser::parse_function_arg)?) - }; - - self.expect_token(&Token::RParen)?; - - let return_type = if self.parse_keyword(Keyword::RETURNS) { - Some(self.parse_data_type()?) - } else { - None - }; - - let params = self.parse_create_function_body()?; - - Ok(Statement::CreateFunction { - or_replace, - temporary, - name, - args, - return_type, - params, - }) + self.parse_postgres_create_function(or_replace, temporary) } else if dialect_of!(self is DuckDbDialect) { self.parse_create_macro(or_replace, temporary) + } else if dialect_of!(self is BigQueryDialect) { + self.parse_bigquery_create_function(or_replace, temporary) } else { self.prev_token(); self.expected("an object type after CREATE", self.peek_token()) } } - fn parse_function_arg(&mut self) -> Result { - let mode = if self.parse_keyword(Keyword::IN) { - Some(ArgMode::In) - } else if self.parse_keyword(Keyword::OUT) { - Some(ArgMode::Out) - } else if self.parse_keyword(Keyword::INOUT) { - Some(ArgMode::InOut) - } else { + /// Parse `CREATE FUNCTION` for [Postgres] + /// + /// [Postgres]: https://www.postgresql.org/docs/15/sql-createfunction.html + fn parse_postgres_create_function( + &mut self, + or_replace: bool, + temporary: bool, + ) -> Result { + let name = self.parse_object_name(false)?; + self.expect_token(&Token::LParen)?; + let args = if self.consume_token(&Token::RParen) { + self.prev_token(); None + } else { + Some(self.parse_comma_separated(Parser::parse_function_arg)?) }; - // parse: [ argname ] argtype - let mut name = None; - let mut data_type = self.parse_data_type()?; - if let DataType::Custom(n, _) = &data_type { - // the first token is actually a name - name = Some(n.0[0].clone()); - data_type = self.parse_data_type()?; - } + self.expect_token(&Token::RParen)?; - let default_expr = if self.parse_keyword(Keyword::DEFAULT) || self.consume_token(&Token::Eq) - { - Some(self.parse_expr()?) + let return_type = if self.parse_keyword(Keyword::RETURNS) { + Some(self.parse_data_type()?) } else { None }; - Ok(OperateFunctionArg { - mode, - name, - data_type, - default_expr, - }) - } - fn parse_create_function_body(&mut self) -> Result { - let mut body = CreateFunctionBody::default(); + #[derive(Default)] + struct Body { + language: Option, + behavior: Option, + function_body: Option, + called_on_null: Option, + parallel: Option, + } + let mut body = Body::default(); loop { fn ensure_not_set(field: &Option, name: &str) -> Result<(), ParserError> { if field.is_some() { @@ -3689,8 +3647,10 @@ impl<'a> Parser<'a> { Ok(()) } if self.parse_keyword(Keyword::AS) { - ensure_not_set(&body.as_, "AS")?; - body.as_ = Some(self.parse_function_definition()?); + ensure_not_set(&body.function_body, "AS")?; + body.function_body = Some(CreateFunctionBody::AsBeforeOptions( + 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(false)?); @@ -3744,12 +3704,186 @@ impl<'a> Parser<'a> { return self.expected("one of UNSAFE | RESTRICTED | SAFE", self.peek_token()); } } else if self.parse_keyword(Keyword::RETURN) { - ensure_not_set(&body.return_, "RETURN")?; - body.return_ = Some(self.parse_expr()?); + ensure_not_set(&body.function_body, "RETURN")?; + body.function_body = Some(CreateFunctionBody::Return(self.parse_expr()?)); + } else { + break; + } + } + + Ok(Statement::CreateFunction { + or_replace, + temporary, + name, + args, + return_type, + behavior: body.behavior, + called_on_null: body.called_on_null, + parallel: body.parallel, + language: body.language, + function_body: body.function_body, + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + } + + /// Parse `CREATE FUNCTION` for [Hive] + /// + /// [Hive]: https://cwiki.apache.org/confluence/display/hive/languagemanual+ddl#LanguageManualDDL-Create/Drop/ReloadFunction + fn parse_hive_create_function( + &mut self, + or_replace: bool, + temporary: bool, + ) -> Result { + let name = self.parse_object_name(false)?; + self.expect_keyword(Keyword::AS)?; + + let as_ = self.parse_create_function_body_string()?; + let using = self.parse_optional_create_function_using()?; + + Ok(Statement::CreateFunction { + or_replace, + temporary, + name, + function_body: Some(CreateFunctionBody::AsBeforeOptions(as_)), + using, + if_not_exists: false, + args: None, + return_type: None, + behavior: None, + called_on_null: None, + parallel: None, + language: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + } + + /// Parse `CREATE FUNCTION` for [BigQuery] + /// + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_function_statement + fn parse_bigquery_create_function( + &mut self, + or_replace: bool, + temporary: bool, + ) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + + let parse_function_param = + |parser: &mut Parser| -> Result { + let name = parser.parse_identifier(false)?; + let data_type = parser.parse_data_type()?; + Ok(OperateFunctionArg { + mode: None, + name: Some(name), + data_type, + default_expr: None, + }) + }; + self.expect_token(&Token::LParen)?; + let args = self.parse_comma_separated0(parse_function_param)?; + self.expect_token(&Token::RParen)?; + + let return_type = if self.parse_keyword(Keyword::RETURNS) { + Some(self.parse_data_type()?) + } else { + None + }; + + let determinism_specifier = if self.parse_keyword(Keyword::DETERMINISTIC) { + Some(FunctionDeterminismSpecifier::Deterministic) + } else if self.parse_keywords(&[Keyword::NOT, Keyword::DETERMINISTIC]) { + Some(FunctionDeterminismSpecifier::NotDeterministic) + } else { + None + }; + + let language = if self.parse_keyword(Keyword::LANGUAGE) { + Some(self.parse_identifier(false)?) + } else { + None + }; + + let remote_connection = + if self.parse_keywords(&[Keyword::REMOTE, Keyword::WITH, Keyword::CONNECTION]) { + Some(self.parse_object_name(false)?) } else { - return Ok(body); + None + }; + + // `OPTIONS` may come before of after the function body but + // may be specified at most once. + let mut options = self.maybe_parse_options(Keyword::OPTIONS)?; + + let function_body = if remote_connection.is_none() { + self.expect_keyword(Keyword::AS)?; + let expr = self.parse_expr()?; + if options.is_none() { + options = self.maybe_parse_options(Keyword::OPTIONS)?; + Some(CreateFunctionBody::AsBeforeOptions(expr)) + } else { + Some(CreateFunctionBody::AsAfterOptions(expr)) } + } else { + None + }; + + Ok(Statement::CreateFunction { + or_replace, + temporary, + if_not_exists, + name, + args: Some(args), + return_type, + function_body, + language, + determinism_specifier, + options, + remote_connection, + using: None, + behavior: None, + called_on_null: None, + parallel: None, + }) + } + + fn parse_function_arg(&mut self) -> Result { + let mode = if self.parse_keyword(Keyword::IN) { + Some(ArgMode::In) + } else if self.parse_keyword(Keyword::OUT) { + Some(ArgMode::Out) + } else if self.parse_keyword(Keyword::INOUT) { + Some(ArgMode::InOut) + } else { + None + }; + + // parse: [ argname ] argtype + let mut name = None; + let mut data_type = self.parse_data_type()?; + if let DataType::Custom(n, _) = &data_type { + // the first token is actually a name + name = Some(n.0[0].clone()); + data_type = self.parse_data_type()?; } + + let default_expr = if self.parse_keyword(Keyword::DEFAULT) || self.consume_token(&Token::Eq) + { + Some(self.parse_expr()?) + } else { + None + }; + Ok(OperateFunctionArg { + mode, + name, + data_type, + default_expr, + }) } pub fn parse_create_macro( @@ -3893,12 +4027,9 @@ impl<'a> Parser<'a> { }; if dialect_of!(self is BigQueryDialect | GenericDialect) { - if let Token::Word(word) = self.peek_token().token { - if word.keyword == Keyword::OPTIONS { - let opts = self.parse_options(Keyword::OPTIONS)?; - if !opts.is_empty() { - options = CreateTableOptions::Options(opts); - } + if let Some(opts) = self.maybe_parse_options(Keyword::OPTIONS)? { + if !opts.is_empty() { + options = CreateTableOptions::Options(opts); } }; } @@ -5680,6 +5811,18 @@ impl<'a> Parser<'a> { } } + pub fn maybe_parse_options( + &mut self, + keyword: Keyword, + ) -> Result>, ParserError> { + if let Token::Word(word) = self.peek_token().token { + if word.keyword == keyword { + return Ok(Some(self.parse_options(keyword)?)); + } + }; + Ok(None) + } + pub fn parse_options(&mut self, keyword: Keyword) -> Result, ParserError> { if self.parse_keyword(keyword) { self.expect_token(&Token::LParen)?; @@ -6521,19 +6664,22 @@ impl<'a> Parser<'a> { } } - pub fn parse_function_definition(&mut self) -> Result { + /// 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(); match peek_token.token { - Token::DollarQuotedString(value) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => + Token::DollarQuotedString(s) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { self.next_token(); - Ok(FunctionDefinition::DoubleDollarDef(value.value)) + Ok(Expr::Value(Value::DollarQuotedString(s))) } - _ => Ok(FunctionDefinition::SingleQuotedDef( + _ => Ok(Expr::Value(Value::SingleQuotedString( self.parse_literal_string()?, - )), + ))), } } + /// Parse a literal string pub fn parse_literal_string(&mut self) -> Result { let next_token = self.next_token(); diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 179755e0c..0bb91cb36 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1950,6 +1950,145 @@ fn parse_map_access_expr() { bigquery().verified_only_select(sql); } +#[test] +fn test_bigquery_create_function() { + let sql = concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION ", + "project1.mydataset.myfunction(x FLOAT64) ", + "RETURNS FLOAT64 ", + "OPTIONS(x = 'y') ", + "AS 42" + ); + + let stmt = bigquery().verified_stmt(sql); + assert_eq!( + stmt, + Statement::CreateFunction { + or_replace: true, + temporary: true, + if_not_exists: false, + name: ObjectName(vec![ + Ident::new("project1"), + Ident::new("mydataset"), + Ident::new("myfunction"), + ]), + args: Some(vec![OperateFunctionArg::with_name("x", DataType::Float64),]), + return_type: Some(DataType::Float64), + function_body: Some(CreateFunctionBody::AsAfterOptions(Expr::Value(number( + "42" + )))), + options: Some(vec![SqlOption { + name: Ident::new("x"), + value: Expr::Value(Value::SingleQuotedString("y".into())), + }]), + behavior: None, + using: None, + language: None, + determinism_specifier: None, + remote_connection: None, + called_on_null: None, + parallel: None, + } + ); + + let sqls = [ + // Arbitrary Options expressions. + concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION ", + "myfunction(a FLOAT64, b INT64, c STRING) ", + "RETURNS ARRAY ", + "OPTIONS(a = [1, 2], b = 'two', c = [('k1', 'v1'), ('k2', 'v2')]) ", + "AS ((SELECT 1 FROM mytable))" + ), + // Options after body. + concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION ", + "myfunction(a FLOAT64, b INT64, c STRING) ", + "RETURNS ARRAY ", + "AS ((SELECT 1 FROM mytable)) ", + "OPTIONS(a = [1, 2], b = 'two', c = [('k1', 'v1'), ('k2', 'v2')])", + ), + // IF NOT EXISTS + concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION IF NOT EXISTS ", + "myfunction(a FLOAT64, b INT64, c STRING) ", + "RETURNS ARRAY ", + "OPTIONS(a = [1, 2]) ", + "AS ((SELECT 1 FROM mytable))" + ), + // No return type. + concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION ", + "myfunction(a FLOAT64, b INT64, c STRING) ", + "OPTIONS(a = [1, 2]) ", + "AS ((SELECT 1 FROM mytable))" + ), + // With language - body after options + concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION ", + "myfunction(a FLOAT64, b INT64, c STRING) ", + "DETERMINISTIC ", + "LANGUAGE js ", + "OPTIONS(a = [1, 2]) ", + "AS \"console.log('hello');\"" + ), + // With language - body before options + concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION ", + "myfunction(a FLOAT64, b INT64, c STRING) ", + "NOT DETERMINISTIC ", + "LANGUAGE js ", + "AS \"console.log('hello');\" ", + "OPTIONS(a = [1, 2])", + ), + // Remote + concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION ", + "myfunction(a FLOAT64, b INT64, c STRING) ", + "RETURNS INT64 ", + "REMOTE WITH CONNECTION us.myconnection ", + "OPTIONS(a = [1, 2])", + ), + ]; + for sql in sqls { + bigquery().verified_stmt(sql); + } + + let error_sqls = [ + ( + concat!( + "CREATE TEMPORARY FUNCTION myfunction() ", + "OPTIONS(a = [1, 2]) ", + "AS ((SELECT 1 FROM mytable)) ", + "OPTIONS(a = [1, 2])", + ), + "Expected end of statement, found: OPTIONS", + ), + ( + concat!( + "CREATE TEMPORARY FUNCTION myfunction() ", + "IMMUTABLE ", + "AS ((SELECT 1 FROM mytable)) ", + ), + "Expected AS, found: IMMUTABLE", + ), + ( + concat!( + "CREATE TEMPORARY FUNCTION myfunction() ", + "AS \"console.log('hello');\" ", + "LANGUAGE js ", + ), + "Expected end of statement, found: LANGUAGE", + ), + ]; + for (sql, error) in error_sqls { + assert_eq!( + ParserError::ParserError(error.to_owned()), + bigquery().parse_sql_statements(sql).unwrap_err() + ); + } +} + #[test] fn test_bigquery_trim() { let real_sql = r#"SELECT customer_id, TRIM(item_price_id, '"', "a") AS item_price_id FROM models_staging.subscriptions"#; diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 78db48ec2..b661b6cd3 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -17,8 +17,8 @@ use sqlparser::ast::{ CreateFunctionBody, CreateFunctionUsing, Expr, Function, FunctionArgumentList, - FunctionArguments, FunctionDefinition, Ident, ObjectName, OneOrManyWithParens, SelectItem, - Statement, TableFactor, UnaryOperator, + FunctionArguments, Ident, ObjectName, OneOrManyWithParens, SelectItem, Statement, TableFactor, + UnaryOperator, Value, }; use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect}; use sqlparser::parser::{ParserError, ParserOptions}; @@ -296,22 +296,23 @@ fn parse_create_function() { Statement::CreateFunction { temporary, name, - params, + function_body, + using, .. } => { assert!(temporary); assert_eq!(name.to_string(), "mydb.myfunc"); assert_eq!( - params, - CreateFunctionBody { - as_: Some(FunctionDefinition::SingleQuotedDef( - "org.random.class.Name".to_string() - )), - using: Some(CreateFunctionUsing::Jar( - "hdfs://somewhere.com:8020/very/far".to_string() - )), - ..Default::default() - } + function_body, + Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( + Value::SingleQuotedString("org.random.class.Name".to_string()) + ))) + ); + assert_eq!( + using, + Some(CreateFunctionUsing::Jar( + "hdfs://somewhere.com:8020/very/far".to_string() + )), ) } _ => unreachable!(), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5c3b653dd..ffcd783f0 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3285,16 +3285,18 @@ fn parse_create_function() { OperateFunctionArg::unnamed(DataType::Integer(None)), ]), return_type: Some(DataType::Integer(None)), - params: CreateFunctionBody { - language: Some("SQL".into()), - behavior: Some(FunctionBehavior::Immutable), - called_on_null: Some(FunctionCalledOnNull::Strict), - parallel: Some(FunctionParallel::Safe), - as_: Some(FunctionDefinition::SingleQuotedDef( - "select $1 + $2;".into() - )), - ..Default::default() - }, + language: Some("SQL".into()), + 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()) + ))), + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, } ); }