Repository: tobymao/sqlglot

Issue:
RETURNS NULL ON NULL INPUT for CREATE FUNCTION in Snowflake Dialect is not supported
**Before you file an issue**
- Make sure you specify the "read" dialect eg. `parse_one(sql, read="spark")`
- Make sure you specify the "write" dialect eg. `ast.sql(dialect="duckdb")`
- Check if the issue still exists on main

**Fully reproducible code snippet**
Please include a fully reproducible code snippet or the input sql, dialect, and expected output.
```shell
pip install 'sqlglot@git+ssh://git@github.com/tobymao/sqlglot.git'
```
```python
create_text = """
CREATE OR REPLACE FUNCTION ibis_udfs.public.object_values("obj" OBJECT)
RETURNS ARRAY
LANGUAGE JAVASCRIPT
RETURNS NULL ON NULL INPUT
AS
$$ return Object.values(obj) $$
"""
import sqlglot
sqlglot.parse(create_text, dialect='snowflake')
```

```
sqlglot.errors.ParseError: Required keyword: 'this' missing for <class 'sqlglot.expressions.InputModelProperty'>. Line 6, Col: 2.
```
**Official Documentation**
Please include links to official SQL documentation related to your issue.
https://docs.snowflake.com/en/sql-reference/sql/create-function#optional-parameters



Discussion:
`STRICT` keyword (alias for `RETURNS NULL ON NULL INPUT`) is also not supported correctly;

```pycon
>>> create_text = """
... CREATE OR REPLACE FUNCTION ibis_udfs.public.object_values("obj" OBJECT)
... RETURNS ARRAY
... LANGUAGE JAVASCRIPT
... STRICT
... AS
... $$ return Object.values(obj) $$
... """
>>> expr = sqlglot.parse_one(create_text, dialect='snowflake')
>>> expr
Create(
  this=UserDefinedFunction(
    this=Dot(
      this=Dot(
        this=Identifier(this=ibis_udfs, quoted=False),
        expression=Identifier(this=public, quoted=False)),
      expression=Identifier(this=object_values, quoted=False)),
    expressions=[
      ColumnDef(
        this=Identifier(this=obj, quoted=True),
        kind=DataType(this=Type.OBJECT, nested=True))],
    wrapped=True),
  kind=FUNCTION,
  replace=True,
  expression=Alias(
    this=Column(
      this=Identifier(this=STRICT, quoted=False)),
    alias=Identifier(this=return Object.values(obj) , quoted=False)),
  properties=Properties(
    expressions=[
      ReturnsProperty(
        this=DataType(this=Type.ARRAY, nested=True)),
      LanguageProperty(
        this=Var(this=JAVASCRIPT))]))
>>> print(expr)
CREATE OR REPLACE FUNCTION ibis_udfs.public.object_values("obj" OBJECT) RETURNS ARRAY LANGUAGE JAVASCRIPT AS STRICT AS  return Object.values(obj)
```

```
CREATE OR REPLACE FUNCTION ibis_udfs.public.object_values("obj" OBJECT) RETURNS ARRAY LANGUAGE JAVASCRIPT AS STRICT AS  return Object.values(obj)
```
raises the error:
```
Syntax error: unexpected 'STRICT'. (line 1)
```
this isn’t high priority for us, feel free to submit a pr for this. 

we welcome well documented, tested, and conforming prs, but won’t have time to implement this one for you

Resolving patch:
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -2682,7 +2682,11 @@ class RemoteWithConnectionModelProperty(Property):
 
 
 class ReturnsProperty(Property):
-    arg_types = {"this": True, "is_table": False, "table": False}
+    arg_types = {"this": False, "is_table": False, "table": False, "null": False}
+
+
+class StrictProperty(Property):
+    arg_types = {}
 
 
 class RowFormatProperty(Property):
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -125,7 +125,9 @@ class Generator(metaclass=_Generator):
         exp.PathColumnConstraint: lambda self, e: f"PATH {self.sql(e, 'this')}",
         exp.RemoteWithConnectionModelProperty: lambda self,
         e: f"REMOTE WITH CONNECTION {self.sql(e, 'this')}",
-        exp.ReturnsProperty: lambda self, e: self.naked_property(e),
+        exp.ReturnsProperty: lambda self, e: (
+            "RETURNS NULL ON NULL INPUT" if e.args.get("null") else self.naked_property(e)
+        ),
         exp.SampleProperty: lambda self, e: f"SAMPLE BY {self.sql(e, 'this')}",
         exp.SetConfigProperty: lambda self, e: self.sql(e, "this"),
         exp.SetProperty: lambda _, e: f"{'MULTI' if e.args.get('multi') else ''}SET",
@@ -135,6 +137,7 @@ class Generator(metaclass=_Generator):
         exp.SqlSecurityProperty: lambda _,
         e: f"SQL SECURITY {'DEFINER' if e.args.get('definer') else 'INVOKER'}",
         exp.StabilityProperty: lambda _, e: e.name,
+        exp.StrictProperty: lambda *_: "STRICT",
         exp.TemporaryProperty: lambda *_: "TEMPORARY",
         exp.TitleColumnConstraint: lambda self, e: f"TITLE {self.sql(e, 'this')}",
         exp.Timestamp: lambda self, e: self.func("TIMESTAMP", e.this, e.expression),
@@ -470,6 +473,7 @@ class Generator(metaclass=_Generator):
         exp.SqlReadWriteProperty: exp.Properties.Location.POST_SCHEMA,
         exp.SqlSecurityProperty: exp.Properties.Location.POST_CREATE,
         exp.StabilityProperty: exp.Properties.Location.POST_SCHEMA,
+        exp.StrictProperty: exp.Properties.Location.POST_SCHEMA,
         exp.TemporaryProperty: exp.Properties.Location.POST_CREATE,
         exp.ToTableProperty: exp.Properties.Location.POST_SCHEMA,
         exp.TransientProperty: exp.Properties.Location.POST_CREATE,
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -838,6 +838,7 @@ class Parser(metaclass=_Parser):
         "READS": lambda self: self._parse_reads_property(),
         "REMOTE": lambda self: self._parse_remote_with_connection(),
         "RETURNS": lambda self: self._parse_returns(),
+        "STRICT": lambda self: self.expression(exp.StrictProperty),
         "ROW": lambda self: self._parse_row(),
         "ROW_FORMAT": lambda self: self._parse_property_assignment(exp.RowFormatProperty),
         "SAMPLE": lambda self: self.expression(
@@ -2262,6 +2263,7 @@ def _parse_remote_with_connection(self) -> exp.RemoteWithConnectionModelProperty
 
     def _parse_returns(self) -> exp.ReturnsProperty:
         value: t.Optional[exp.Expression]
+        null = None
         is_table = self._match(TokenType.TABLE)
 
         if is_table:
@@ -2275,10 +2277,13 @@ def _parse_returns(self) -> exp.ReturnsProperty:
                     self.raise_error("Expecting >")
             else:
                 value = self._parse_schema(exp.var("TABLE"))
+        elif self._match_text_seq("NULL", "ON", "NULL", "INPUT"):
+            null = True
+            value = None
         else:
             value = self._parse_types()
 
-        return self.expression(exp.ReturnsProperty, this=value, is_table=is_table)
+        return self.expression(exp.ReturnsProperty, this=value, is_table=is_table, null=null)
 
     def _parse_describe(self) -> exp.Describe:
         kind = self._match_set(self.CREATABLES) and self._prev.text


Test patch:
diff --git a/tests/dialects/test_postgres.py b/tests/dialects/test_postgres.py
--- a/tests/dialects/test_postgres.py
+++ b/tests/dialects/test_postgres.py
@@ -798,23 +798,21 @@ def test_ddl(self):
             "CREATE TABLE test (x TIMESTAMP[][])",
         )
         self.validate_identity(
-            "CREATE FUNCTION add(INT, INT) RETURNS INT SET search_path TO 'public' AS 'select $1 + $2;' LANGUAGE SQL IMMUTABLE",
-            check_command_warning=True,
+            "CREATE FUNCTION add(integer, integer) RETURNS INT LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT AS 'select $1 + $2;'",
         )
         self.validate_identity(
-            "CREATE FUNCTION x(INT) RETURNS INT SET foo FROM CURRENT",
-            check_command_warning=True,
+            "CREATE FUNCTION add(integer, integer) RETURNS INT LANGUAGE SQL IMMUTABLE STRICT AS 'select $1 + $2;'"
         )
         self.validate_identity(
-            "CREATE FUNCTION add(integer, integer) RETURNS integer AS 'select $1 + $2;' LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT",
+            "CREATE FUNCTION add(INT, INT) RETURNS INT SET search_path TO 'public' AS 'select $1 + $2;' LANGUAGE SQL IMMUTABLE",
             check_command_warning=True,
         )
         self.validate_identity(
-            "CREATE FUNCTION add(integer, integer) RETURNS integer AS 'select $1 + $2;' LANGUAGE SQL IMMUTABLE CALLED ON NULL INPUT",
+            "CREATE FUNCTION x(INT) RETURNS INT SET foo FROM CURRENT",
             check_command_warning=True,
         )
         self.validate_identity(
-            "CREATE FUNCTION add(integer, integer) RETURNS integer AS 'select $1 + $2;' LANGUAGE SQL IMMUTABLE STRICT",
+            "CREATE FUNCTION add(integer, integer) RETURNS integer AS 'select $1 + $2;' LANGUAGE SQL IMMUTABLE CALLED ON NULL INPUT",
             check_command_warning=True,
         )
         self.validate_identity(
diff --git a/tests/dialects/test_snowflake.py b/tests/dialects/test_snowflake.py
--- a/tests/dialects/test_snowflake.py
+++ b/tests/dialects/test_snowflake.py
@@ -1228,6 +1228,12 @@ def test_ddl(self):
         self.validate_identity(
             "CREATE ICEBERG TABLE my_iceberg_table (amount ARRAY(INT)) CATALOG='SNOWFLAKE' EXTERNAL_VOLUME='my_external_volume' BASE_LOCATION='my/relative/path/from/extvol'"
         )
+        self.validate_identity(
+            """CREATE OR REPLACE FUNCTION ibis_udfs.public.object_values("obj" OBJECT) RETURNS ARRAY LANGUAGE JAVASCRIPT RETURNS NULL ON NULL INPUT AS ' return Object.values(obj) '"""
+        )
+        self.validate_identity(
+            """CREATE OR REPLACE FUNCTION ibis_udfs.public.object_values("obj" OBJECT) RETURNS ARRAY LANGUAGE JAVASCRIPT STRICT AS ' return Object.values(obj) '"""
+        )
         self.validate_identity(
             "CREATE OR REPLACE FUNCTION my_udf(location OBJECT(city VARCHAR, zipcode DECIMAL(38, 0), val ARRAY(BOOLEAN))) RETURNS VARCHAR AS $$ SELECT 'foo' $$",
             "CREATE OR REPLACE FUNCTION my_udf(location OBJECT(city VARCHAR, zipcode DECIMAL(38, 0), val ARRAY(BOOLEAN))) RETURNS VARCHAR AS ' SELECT \\'foo\\' '",