diff --git a/ibis/backends/oracle/__init__.py b/ibis/backends/oracle/__init__.py index 8b6d97a630c0..38617b7959f1 100644 --- 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, diff --git a/ibis/backends/tests/test_client.py b/ibis/backends/tests/test_client.py index e9aeb32d1c81..df7031ea0c0f 100644 --- 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 index 13e8b418a176..faf8e554d0ab 100644 --- 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")