Repository: tobymao/sqlglot

Issue:
Problem with Teradata create volatile table statement
There is a problem with parsing the Teradata CREATE VOLATILE TABLE statement.
In Teradata version 16.20 this SQL will work correctly:

create multiset volatile table my_table (
  id int
) 
primary index (id) 
on commit preserve rows ;

This SQL will fail
create multiset volatile table my_table (
  id int
) 
on commit preserve rows 
primary index (id) ;
With the Error "Expected something between the ROWS keyword and the PRIMARY keyword"

In sqlglot using teradata
The first statement will fail but the second incorrect statement will parse correctly. 

The error from the first statement is:
File ~\AppData\Roaming\Python\Python311\site-packages\sqlglot\parser.py:851, in Parser.parse(self, raw_tokens, sql)
    837 def parse(
    838     self, raw_tokens: t.List[Token], sql: t.Optional[str] = None
    839 ) -> t.List[t.Optional[exp.Expression]]:
    840     """
    841     Parses a list of tokens and returns a list of syntax trees, one tree
    842     per parsed SQL statement.
   (...)
    849         The list of syntax trees.
    850     """
--> 851     return self._parse(
    852         parse_method=self.__class__._parse_statement, raw_tokens=raw_tokens, sql=sql
    853     )

File ~\AppData\Roaming\Python\Python311\site-packages\sqlglot\parser.py:917, in Parser._parse(self, parse_method, raw_tokens, sql)
    914     expressions.append(parse_method(self))
    916     if self._index < len(self._tokens):
--> 917         self.raise_error("Invalid expression / Unexpected token")
    919     self.check_errors()
    921 return expressions

File ~\AppData\Roaming\Python\Python311\site-packages\sqlglot\parser.py:960, in Parser.raise_error(self, message, token)
    948 error = ParseError.new(
    949     f"{message}. Line {token.line}, Col: {token.col}.\n"
    950     f"  {start_context}\033[4m{highlight}\033[0m{end_context}",
   (...)
    956     end_context=end_context,
    957 )
    959 if self.error_level == ErrorLevel.IMMEDIATE:
--> 960     raise error
    962 self.errors.append(error)

ParseError: Invalid expression / Unexpected token. Line 6, Col: 2.
  





Comments:

Patch:
diff --git a/sqlglot/dialects/teradata.py b/sqlglot/dialects/teradata.py
index 3bed58bb..db0d9d4c 100644
--- a/sqlglot/dialects/teradata.py
+++ b/sqlglot/dialects/teradata.py
@@ -141,7 +141,9 @@ class Teradata(Dialect):
 
         PROPERTIES_LOCATION = {
             **generator.Generator.PROPERTIES_LOCATION,  # type: ignore
+            exp.OnCommitProperty: exp.Properties.Location.POST_INDEX,
             exp.PartitionedByProperty: exp.Properties.Location.POST_INDEX,
+            exp.StabilityProperty: exp.Properties.Location.POST_CREATE,
         }
 
         TRANSFORMS = {
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index 0fff2dca..5f890ac7 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -1896,7 +1896,7 @@ class NoPrimaryIndexProperty(Property):
 
 
 class OnCommitProperty(Property):
-    arg_type = {"this": False}
+    arg_type = {"delete": False}
 
 
 class PartitionedByProperty(Property):
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index 3b9961bb..bb94a51f 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -76,7 +76,7 @@ class Generator:
         exp.LogProperty: lambda self, e: f"{'NO ' if e.args.get('no') else ''}LOG",
         exp.MaterializedProperty: lambda self, e: "MATERIALIZED",
         exp.NoPrimaryIndexProperty: lambda self, e: "NO PRIMARY INDEX",
-        exp.OnCommitProperty: lambda self, e: "ON COMMIT PRESERVE ROWS",
+        exp.OnCommitProperty: lambda self, e: f"ON COMMIT {'DELETE' if e.args.get('delete') else 'PRESERVE'} ROWS",
         exp.ReturnsProperty: lambda self, e: self.naked_property(e),
         exp.SetProperty: lambda self, e: f"{'MULTI' if e.args.get('multi') else ''}SET",
         exp.SettingsProperty: lambda self, e: f"SETTINGS{self.seg('')}{(self.expressions(e))}",
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index b4382539..54c152e9 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -1212,28 +1212,21 @@ class Parser(metaclass=_Parser):
             expression = self._parse_ddl_select()
 
             if create_token.token_type == TokenType.TABLE:
-                # exp.Properties.Location.POST_EXPRESSION
-                temp_properties = self._parse_properties()
-                if properties and temp_properties:
-                    properties.expressions.extend(temp_properties.expressions)
-                elif temp_properties:
-                    properties = temp_properties
-
                 indexes = []
                 while True:
                     index = self._parse_create_table_index()
 
-                    # exp.Properties.Location.POST_INDEX
-                    if self._match(TokenType.PARTITION_BY, advance=False):
-                        temp_properties = self._parse_properties()
-                        if properties and temp_properties:
-                            properties.expressions.extend(temp_properties.expressions)
-                        elif temp_properties:
-                            properties = temp_properties
+                    # exp.Properties.Location.POST_EXPRESSION or exp.Properties.Location.POST_INDEX
+                    temp_properties = self._parse_properties()
+                    if properties and temp_properties:
+                        properties.expressions.extend(temp_properties.expressions)
+                    elif temp_properties:
+                        properties = temp_properties
 
                     if not index:
                         break
                     else:
+                        self._match(TokenType.COMMA)
                         indexes.append(index)
             elif create_token.token_type == TokenType.VIEW:
                 if self._match_text_seq("WITH", "NO", "SCHEMA", "BINDING"):
@@ -1589,8 +1582,9 @@ class Parser(metaclass=_Parser):
         return exp.NoPrimaryIndexProperty()
 
     def _parse_oncommit(self) -> exp.Expression:
-        self._match_text_seq("COMMIT", "PRESERVE", "ROWS")
-        return exp.OnCommitProperty()
+        if self._match_text_seq("COMMIT", "PRESERVE", "ROWS"):
+            return exp.OnCommitProperty()
+        return exp.OnCommitProperty(delete=self._match_text_seq("COMMIT", "DELETE", "ROWS"))
 
     def _parse_distkey(self) -> exp.Expression:
         return self.expression(exp.DistKeyProperty, this=self._parse_wrapped(self._parse_id_var))


Test patch:
diff --git a/tests/dialects/test_teradata.py b/tests/dialects/test_teradata.py
index dcb513d8..902e360c 100644
--- a/tests/dialects/test_teradata.py
+++ b/tests/dialects/test_teradata.py
@@ -24,6 +24,12 @@ class TestTeradata(Validator):
 
     def test_create(self):
         self.validate_identity("CREATE TABLE x (y INT) PRIMARY INDEX (y) PARTITION BY y INDEX (y)")
+        self.validate_identity(
+            "CREATE MULTISET VOLATILE TABLE my_table (id INT) PRIMARY INDEX (id) ON COMMIT PRESERVE ROWS"
+        )
+        self.validate_identity(
+            "CREATE SET VOLATILE TABLE my_table (id INT) PRIMARY INDEX (id) ON COMMIT DELETE ROWS"
+        )
         self.validate_identity(
             "CREATE TABLE a (b INT) PRIMARY INDEX (y) PARTITION BY RANGE_N(b BETWEEN 'a', 'b' AND 'c' EACH '1')"
         )
@@ -34,11 +40,21 @@ class TestTeradata(Validator):
             "CREATE TABLE a (b INT) PARTITION BY RANGE_N(b BETWEEN *, 1 AND * EACH b) INDEX (a)"
         )
 
+        self.validate_all(
+            """
+            CREATE SET TABLE test, NO FALLBACK, NO BEFORE JOURNAL, NO AFTER JOURNAL,
+            CHECKSUM = DEFAULT (x INT, y INT, z CHAR(30), a INT, b DATE, e INT)
+            PRIMARY INDEX (a),
+            INDEX(x, y)
+            """,
+            write={
+                "teradata": "CREATE SET TABLE test, NO FALLBACK, NO BEFORE JOURNAL, NO AFTER JOURNAL, CHECKSUM=DEFAULT (x INT, y INT, z CHAR(30), a INT, b DATE, e INT) PRIMARY INDEX (a) INDEX (x, y)",
+            },
+        )
         self.validate_all(
             "REPLACE VIEW a AS (SELECT b FROM c)",
             write={"teradata": "CREATE OR REPLACE VIEW a AS (SELECT b FROM c)"},
         )
-
         self.validate_all(
             "CREATE VOLATILE TABLE a",
             write={