This is an automated email from the ASF dual-hosted git repository.

github-bot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 4beea9a4 Support PostgreSQL C Functions with Multiple AS Parameters 
(#2095)
4beea9a4 is described below

commit 4beea9a4bccb867b34f466f0ad1793ebcdf370ba
Author: Luca Cappelletti <[email protected]>
AuthorDate: Wed Nov 26 11:21:57 2025 +0100

    Support PostgreSQL C Functions with Multiple AS Parameters (#2095)
    
    Co-authored-by: Ifeanyi Ubah <[email protected]>
---
 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<Expr>,
+    },
     /// 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<Expr, 
ParserError> {
-        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<CreateFunctionBody, ParserError> {
+        let parse_string_expr = |parser: &mut Parser| -> Result<Expr, 
ParserError> {
+            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 EX [...]
@@ -6081,7 +6145,8 @@ fn parse_trigger_related_functions() {
                             },
                         )
                     ).with_empty_span()),
-                ),
+                    link_symbol: None,
+                },
             ),
             behavior: None,
             called_on_null: None,


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to