Repository: ibis-project/ibis

Issue:
bug: Oracle: DESC / DESCRIBE is a SQL*Plus command only
### What happened?

DESCRIBE is not a valid SQL statement. It is only available in SQL*Plus and SQL Developer. It cannot be used by the client.

It is used in the [ibis/ibis/backends/oracle/__init__.py](https://github.com/ibis-project/ibis/blob/a4a69d0875e52d749f6f674cbce96c896974ab7b/ibis/backends/oracle/__init__.py#L184) in the _metadata method.

More info:
- [here](https://ss64.com/ora/desc.html)
- [and here](https://stackoverflow.com/questions/37133666/what-is-the-reason-for-not-working-of-describe-command-on-oracle)

### What version of ibis are you using?

ibis-framework 7.0.0.dev474 from the main branch

### What backend(s) are you using, if any?

Oracle

### Relevant log output

```sh
File "src/oracledb/impl/thick/cursor.pyx", line 243, in oracledb.thick_impl.ThickCursorImpl.execute
  File "src/oracledb/impl/thick/utils.pyx", line 431, in oracledb.thick_impl._raise_from_odpi
  File "src/oracledb/impl/thick/utils.pyx", line 421, in oracledb.thick_impl._raise_from_info
sqlalchemy.exc.DatabaseError: (oracledb.exceptions.DatabaseError) ORA-00900: invalid SQL statement
[SQL: DESCRIBE ORA$PTT_2be12eac96624008a42cfad09a63902a]
(Background on this error at: https://sqlalche.me/e/14/4xp6)
```


### Code of Conduct

- [X] I agree to follow this project's Code of Conduct
bug: Oracle: Support for setting PRIVATE_TEMP_TABLE_PREFIX for PRIVATE TEMPORARY TABLE
### What happened?

When creating a private temporary table in Oracle, the table name needs to have a predefined prefix (`PRIVATE_TEMP_TABLE_PREFIX`).

In ibis, the prefix is custom (`__ibis_oracle_metadata_`), which requires the user to set this exact name as a parameter in OracleDB.

It would be beneficial if the default in ibis was same as the default in Oracle (`ORA$PTT_`), with an option to change it during the connection initialization. Right now it is not possible and returns errors.

It is used in the [ibis/ibis/backends/oracle/init.py](https://github.com/ibis-project/ibis/blob/a4a69d0875e52d749f6f674cbce96c896974ab7b/ibis/backends/oracle/__init__.py#L178) in the _metadata method.

More info:
- [here](https://oracle-base.com/articles/18c/private-temporary-tables-18c#:~:text=Private%20temporary%20tables%20share%20the,is%20%22ORA%24PTT_%22.)
- [and here](https://docs.oracle.com/en/database/oracle/oracle-database/18/refrn/PRIVATE_TEMP_TABLE_PREFIX.html#GUID-57FE93DA-BFC6-4E9B-B14E-9D5A031157B4)

### What version of ibis are you using?

ibis-framework 7.0.0.dev474 from the main branch

### What backend(s) are you using, if any?

Oracle

### Relevant log output

```sh
File "src/oracledb/impl/thick/cursor.pyx", line 243, in oracledb.thick_impl.ThickCursorImpl.execute
  File "src/oracledb/impl/thick/utils.pyx", line 431, in oracledb.thick_impl._raise_from_odpi
  File "src/oracledb/impl/thick/utils.pyx", line 421, in oracledb.thick_impl._raise_from_info
sqlalchemy.exc.DatabaseError: (oracledb.exceptions.DatabaseError) ORA-00911: invalid character
[SQL: CREATE PRIVATE TEMPORARY TABLE __ibis_oracle_metadata_e094bd07def04efe926d973aa0e36031 AS SELECT 1 AS TEST FROM DUAL]
(Background on this error at: https://sqlalche.me/e/14/4xp6)
```


### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:

Resolving patch:
diff --git a/ibis/backends/oracle/__init__.py b/ibis/backends/oracle/__init__.py
--- a/ibis/backends/oracle/__init__.py
+++ b/ibis/backends/oracle/__init__.py
@@ -175,23 +175,48 @@ def current_database(self) -> str:
         return self._scalar_query("SELECT * FROM global_name")
 
     def _metadata(self, query: str) -> Iterable[tuple[str, dt.DataType]]:
-        table = f"__ibis_oracle_metadata_{util.guid()}"
+        from sqlalchemy_views import CreateView, DropView
+
+        name = util.gen_name("oracle_metadata")
+
+        view = sa.table(name)
+        create_view = CreateView(view, sa.text(query))
+        drop_view = DropView(view, if_exists=True)
+
+        t = sa.table(
+            "all_tab_columns",
+            sa.column("table_name"),
+            sa.column("column_name"),
+            sa.column("data_type"),
+            sa.column("data_precision"),
+            sa.column("data_scale"),
+            sa.column("nullable"),
+        )
+        metadata_query = sa.select(
+            t.c.column_name,
+            t.c.data_type,
+            t.c.data_precision,
+            t.c.data_scale,
+            (t.c.nullable == "Y").label("nullable"),
+        ).where(t.c.table_name == name)
 
         with self.begin() as con:
-            con.exec_driver_sql(
-                f"CREATE PRIVATE TEMPORARY TABLE {table} AS {query.strip(';')}"
-            )
-            result = con.exec_driver_sql(f"DESCRIBE {table}").mappings().all()
-            con.exec_driver_sql(f"DROP TABLE {table}")
-
-        fields = {}
-        for field in result:
-            name = field["Field"]
-            type_string = field["Type"]
-            is_nullable = field["Null"] == "YES"
-            fields[name] = OracleType.from_string(type_string, nullable=is_nullable)
-
-        return sch.Schema(fields)
+            con.execute(create_view)
+            try:
+                results = con.execute(metadata_query).fetchall()
+            finally:
+                # drop the view no matter what
+                con.execute(drop_view)
+
+        for name, type_string, precision, scale, nullable in results:
+            if precision is not None and scale is not None and precision != 0:
+                typ = dt.Decimal(precision=precision, scale=scale, nullable=nullable)
+            elif precision == 0:
+                # TODO: how to disambiguate between int and float here without inspecting the value?
+                typ = dt.float
+            else:
+                typ = OracleType.from_string(type_string, nullable=nullable)
+            yield name, typ
 
     def _table_from_schema(
         self,


Test patch:
diff --git a/ibis/backends/tests/test_client.py b/ibis/backends/tests/test_client.py
--- a/ibis/backends/tests/test_client.py
+++ b/ibis/backends/tests/test_client.py
@@ -169,7 +169,6 @@ def test_query_schema(ddl_backend, expr_fn, expected):
 
 
 @pytest.mark.notimpl(["datafusion", "polars", "mssql"])
-@pytest.mark.notimpl(["oracle"], raises=sa.exc.DatabaseError)
 @pytest.mark.never(["dask", "pandas"], reason="dask and pandas do not support SQL")
 def test_sql(backend, con):
     # execute the expression using SQL query
diff --git a/ibis/backends/tests/test_dot_sql.py b/ibis/backends/tests/test_dot_sql.py
--- a/ibis/backends/tests/test_dot_sql.py
+++ b/ibis/backends/tests/test_dot_sql.py
@@ -295,9 +295,9 @@ def test_con_dot_sql_transpile(backend, con, dialect, df):
 
 
 @dot_sql_notimpl
-@dot_sql_notyet
 @dot_sql_never
 @pytest.mark.notimpl(["druid", "flink", "impala", "polars", "pyspark"])
+@pytest.mark.notyet(["snowflake"], reason="snowflake column names are case insensitive")
 def test_order_by_no_projection(backend):
     con = backend.connection
     astronauts = con.table("astronauts")