Repository: tobymao/sqlglot

Issue:
feat: support for temporary tables in Oracle dialect
**Is your feature request related to a problem? Please describe.**
Oracle requires either `PRIVATE` or `GLOBAL` keyword when creating a temporary table -- the syntax `CREATE TEMPORARY TABLE` is invalid, it should be either `CREATE PRIVATE TEMPORARY TABLE` or `CREATE GLOBAL TEMPORARY TABLE`

**Describe the solution you'd like**
the `PRIVATE` temp tables have a bunch of restrictions and seem unlike most other temporary tables to me -- I think adding `GLOBAL` to the Oracle dialect would get most users where they're likely to want to go.

**Describe alternatives you've considered**
I've got a heinous hack in Ibis where I intercept the output of `self.create_table` and then do a string replace to add in `GLOBAL`, and that works fine, but it's brittle and doesn't help anyone else.

**Additional context**
https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/CREATE-TABLE.html#GUID-F9CE0CC3-13AE-4744-A43C-EAC7A71AAAB6


Discussion:

Patch:
diff --git a/sqlglot/dialects/oracle.py b/sqlglot/dialects/oracle.py
index fcb3aab9..c3888a3b 100644
--- a/sqlglot/dialects/oracle.py
+++ b/sqlglot/dialects/oracle.py
@@ -93,6 +93,14 @@ class Oracle(Dialect):
             "XMLTABLE": lambda self: self._parse_xml_table(),
         }
 
+        PROPERTY_PARSERS = {
+            **parser.Parser.PROPERTY_PARSERS,
+            "GLOBAL": lambda self: self._match_text_seq("TEMPORARY")
+            and self.expression(exp.TemporaryProperty, this="GLOBAL"),
+            "PRIVATE": lambda self: self._match_text_seq("TEMPORARY")
+            and self.expression(exp.TemporaryProperty, this="PRIVATE"),
+        }
+
         QUERY_MODIFIER_PARSERS = {
             **parser.Parser.QUERY_MODIFIER_PARSERS,
             TokenType.ORDER_SIBLINGS_BY: lambda self: ("order", self._parse_order()),
@@ -207,6 +215,7 @@ class Oracle(Dialect):
             exp.Substring: rename_func("SUBSTR"),
             exp.Table: lambda self, e: self.table_sql(e, sep=" "),
             exp.TableSample: lambda self, e: self.tablesample_sql(e, sep=" "),
+            exp.TemporaryProperty: lambda _, e: f"{e.name or 'GLOBAL'} TEMPORARY",
             exp.TimeToStr: lambda self, e: self.func("TO_CHAR", e.this, self.format_time(e)),
             exp.ToChar: lambda self, e: self.function_fallback_sql(e),
             exp.Trim: trim_sql,
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index ee4cdde5..bfffe31d 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -2463,7 +2463,7 @@ class StabilityProperty(Property):
 
 
 class TemporaryProperty(Property):
-    arg_types = {}
+    arg_types = {"this": False}
 
 
 class TransformModelProperty(Property):
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index 66466734..e8307dfa 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -73,7 +73,7 @@ class Generator(metaclass=_Generator):
     TRANSFORMS: t.Dict[t.Type[exp.Expression], t.Callable[..., str]] = {
         **JSON_PATH_PART_TRANSFORMS,
         exp.AutoRefreshProperty: lambda self, e: f"AUTO REFRESH {self.sql(e, 'this')}",
-        exp.CaseSpecificColumnConstraint: lambda self,
+        exp.CaseSpecificColumnConstraint: lambda _,
         e: f"{'NOT ' if e.args.get('not_') else ''}CASESPECIFIC",
         exp.CharacterSetColumnConstraint: lambda self, e: f"CHARACTER SET {self.sql(e, 'this')}",
         exp.CharacterSetProperty: lambda self,
@@ -82,7 +82,7 @@ class Generator(metaclass=_Generator):
         e: f"CLUSTERED ({self.expressions(e, 'this', indent=False)})",
         exp.CollateColumnConstraint: lambda self, e: f"COLLATE {self.sql(e, 'this')}",
         exp.CommentColumnConstraint: lambda self, e: f"COMMENT {self.sql(e, 'this')}",
-        exp.CopyGrantsProperty: lambda self, e: "COPY GRANTS",
+        exp.CopyGrantsProperty: lambda *_: "COPY GRANTS",
         exp.DateAdd: lambda self, e: self.func(
             "DATE_ADD", e.this, e.expression, exp.Literal.string(e.text("unit"))
         ),
@@ -90,8 +90,8 @@ class Generator(metaclass=_Generator):
         exp.DefaultColumnConstraint: lambda self, e: f"DEFAULT {self.sql(e, 'this')}",
         exp.EncodeColumnConstraint: lambda self, e: f"ENCODE {self.sql(e, 'this')}",
         exp.ExecuteAsProperty: lambda self, e: self.naked_property(e),
-        exp.ExternalProperty: lambda self, e: "EXTERNAL",
-        exp.HeapProperty: lambda self, e: "HEAP",
+        exp.ExternalProperty: lambda *_: "EXTERNAL",
+        exp.HeapProperty: lambda *_: "HEAP",
         exp.InheritsProperty: lambda self, e: f"INHERITS ({self.expressions(e, flat=True)})",
         exp.InlineLengthColumnConstraint: lambda self, e: f"INLINE LENGTH {self.sql(e, 'this')}",
         exp.InputModelProperty: lambda self, e: f"INPUT{self.sql(e, 'this')}",
@@ -104,13 +104,13 @@ class Generator(metaclass=_Generator):
         ),
         exp.LanguageProperty: lambda self, e: self.naked_property(e),
         exp.LocationProperty: lambda self, e: self.naked_property(e),
-        exp.LogProperty: lambda self, e: f"{'NO ' if e.args.get('no') else ''}LOG",
-        exp.MaterializedProperty: lambda self, e: "MATERIALIZED",
+        exp.LogProperty: lambda _, e: f"{'NO ' if e.args.get('no') else ''}LOG",
+        exp.MaterializedProperty: lambda *_: "MATERIALIZED",
         exp.NonClusteredColumnConstraint: lambda self,
         e: f"NONCLUSTERED ({self.expressions(e, 'this', indent=False)})",
-        exp.NoPrimaryIndexProperty: lambda self, e: "NO PRIMARY INDEX",
-        exp.NotForReplicationColumnConstraint: lambda self, e: "NOT FOR REPLICATION",
-        exp.OnCommitProperty: lambda self,
+        exp.NoPrimaryIndexProperty: lambda *_: "NO PRIMARY INDEX",
+        exp.NotForReplicationColumnConstraint: lambda *_: "NOT FOR REPLICATION",
+        exp.OnCommitProperty: lambda _,
         e: f"ON COMMIT {'DELETE' if e.args.get('delete') else 'PRESERVE'} ROWS",
         exp.OnProperty: lambda self, e: f"ON {self.sql(e, 'this')}",
         exp.OnUpdateColumnConstraint: lambda self, e: f"ON UPDATE {self.sql(e, 'this')}",
@@ -121,21 +121,21 @@ class Generator(metaclass=_Generator):
         exp.ReturnsProperty: lambda self, e: 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 self, e: f"{'MULTI' if e.args.get('multi') else ''}SET",
+        exp.SetProperty: lambda _, e: f"{'MULTI' if e.args.get('multi') else ''}SET",
         exp.SettingsProperty: lambda self, e: f"SETTINGS{self.seg('')}{(self.expressions(e))}",
-        exp.SqlReadWriteProperty: lambda self, e: e.name,
-        exp.SqlSecurityProperty: lambda self,
+        exp.SqlReadWriteProperty: lambda _, e: e.name,
+        exp.SqlSecurityProperty: lambda _,
         e: f"SQL SECURITY {'DEFINER' if e.args.get('definer') else 'INVOKER'}",
-        exp.StabilityProperty: lambda self, e: e.name,
-        exp.TemporaryProperty: lambda self, e: "TEMPORARY",
+        exp.StabilityProperty: lambda _, e: e.name,
+        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),
         exp.ToTableProperty: lambda self, e: f"TO {self.sql(e.this)}",
         exp.TransformModelProperty: lambda self, e: self.func("TRANSFORM", *e.expressions),
-        exp.TransientProperty: lambda self, e: "TRANSIENT",
-        exp.UppercaseColumnConstraint: lambda self, e: "UPPERCASE",
+        exp.TransientProperty: lambda *_: "TRANSIENT",
+        exp.UppercaseColumnConstraint: lambda *_: "UPPERCASE",
         exp.VarMap: lambda self, e: self.func("MAP", e.args["keys"], e.args["values"]),
-        exp.VolatileProperty: lambda self, e: "VOLATILE",
+        exp.VolatileProperty: lambda *_: "VOLATILE",
         exp.WithJournalTableProperty: lambda self, e: f"WITH JOURNAL TABLE={self.sql(e, 'this')}",
     }
 


Test patch:
diff --git a/tests/dialects/test_oracle.py b/tests/dialects/test_oracle.py
index d49f7e91..9028b031 100644
--- a/tests/dialects/test_oracle.py
+++ b/tests/dialects/test_oracle.py
@@ -1,4 +1,4 @@
-from sqlglot import exp, parse_one
+from sqlglot import exp
 from sqlglot.errors import UnsupportedError
 from tests.dialects.test_dialect import Validator
 
@@ -7,11 +7,11 @@ class TestOracle(Validator):
     dialect = "oracle"
 
     def test_oracle(self):
-        self.validate_identity("REGEXP_REPLACE('source', 'search')")
-        parse_one("ALTER TABLE tbl_name DROP FOREIGN KEY fk_symbol", dialect="oracle").assert_is(
-            exp.AlterTable
-        )
+        self.parse_one("ALTER TABLE tbl_name DROP FOREIGN KEY fk_symbol").assert_is(exp.AlterTable)
 
+        self.validate_identity("CREATE GLOBAL TEMPORARY TABLE t AS SELECT * FROM orders")
+        self.validate_identity("CREATE PRIVATE TEMPORARY TABLE t AS SELECT * FROM orders")
+        self.validate_identity("REGEXP_REPLACE('source', 'search')")
         self.validate_identity("TIMESTAMP(3) WITH TIME ZONE")
         self.validate_identity("CURRENT_TIMESTAMP(precision)")
         self.validate_identity("ALTER TABLE tbl_name DROP FOREIGN KEY fk_symbol")
diff --git a/tests/dialects/test_tsql.py b/tests/dialects/test_tsql.py
index a615c190..a0df87f0 100644
--- a/tests/dialects/test_tsql.py
+++ b/tests/dialects/test_tsql.py
@@ -144,7 +144,7 @@ class TestTSQL(Validator):
                 "tsql": "CREATE TABLE #mytemptable (a INTEGER)",
                 "snowflake": "CREATE TEMPORARY TABLE mytemptable (a INT)",
                 "duckdb": "CREATE TEMPORARY TABLE mytemptable (a INT)",
-                "oracle": "CREATE TEMPORARY TABLE mytemptable (a NUMBER)",
+                "oracle": "CREATE GLOBAL TEMPORARY TABLE mytemptable (a NUMBER)",
                 "hive": "CREATE TEMPORARY TABLE mytemptable (a INT)",
                 "spark2": "CREATE TEMPORARY TABLE mytemptable (a INT) USING PARQUET",
                 "spark": "CREATE TEMPORARY TABLE mytemptable (a INT) USING PARQUET",