Repository: ibis-project/ibis

Issue:
feat(sql): move to temporary views for `con.read_x` APIs to avoid view proliferation
### Is your feature request related to a problem?

Not related to a problem.

### Describe the solution you'd like

Now that we're using a static connection pool, we should be able to use temporary views again to avoid pooping a bunch of random-looking views that persist in a database over time.

### What version of ibis are you running?

`master`

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

DuckDB

### Code of Conduct

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


Discussion:

Resolving patch:
diff --git a/ibis/backends/duckdb/__init__.py b/ibis/backends/duckdb/__init__.py
--- a/ibis/backends/duckdb/__init__.py
+++ b/ibis/backends/duckdb/__init__.py
@@ -238,6 +238,12 @@ def _register_failure(self):
             f"please call one of {msg} directly"
         )
 
+    def _compile_temp_view(self, table_name, source):
+        raw_source = source.compile(
+            dialect=self.con.dialect, compile_kwargs=dict(literal_binds=True)
+        )
+        return f'CREATE OR REPLACE TEMPORARY VIEW "{table_name}" AS {raw_source}'
+
     @util.experimental
     def read_json(
         self,
@@ -263,7 +269,6 @@ def read_json(
         Table
             An ibis table expression
         """
-        import sqlalchemy_views as sav
         from packaging.version import parse as vparse
 
         if (version := vparse(self.version)) < vparse("0.7.0"):
@@ -273,18 +278,15 @@ def read_json(
         if not table_name:
             table_name = f"ibis_read_json_{next(json_n)}"
 
-        view = sav.CreateView(
-            sa.table(table_name),
-            sa.select(sa.literal_column("*")).select_from(
-                sa.func.read_json_auto(
-                    sa.func.list_value(*normalize_filenames(source_list)),
-                    _format_kwargs(kwargs),
-                )
-            ),
-            or_replace=True,
+        source = sa.select(sa.literal_column("*")).select_from(
+            sa.func.read_json_auto(
+                sa.func.list_value(*normalize_filenames(source_list)),
+                _format_kwargs(kwargs),
+            )
         )
+        view = self._compile_temp_view(table_name, source)
         with self.begin() as con:
-            con.execute(view)
+            con.exec_driver_sql(view)
 
         return self.table(table_name)
 
@@ -313,8 +315,6 @@ def read_csv(
         ir.Table
             The just-registered table
         """
-        import sqlalchemy_views as sav
-
         source_list = normalize_filenames(source_list)
 
         if not table_name:
@@ -329,9 +329,10 @@ def read_csv(
         source = sa.select(sa.literal_column("*")).select_from(
             sa.func.read_csv(sa.func.list_value(*source_list), _format_kwargs(kwargs))
         )
-        view = sav.CreateView(sa.table(table_name), source, or_replace=True)
+
+        view = self._compile_temp_view(table_name, source)
         with self.begin() as con:
-            con.execute(view)
+            con.exec_driver_sql(view)
         return self.table(table_name)
 
     def read_parquet(
@@ -377,13 +378,8 @@ def read_parquet(
         return self.table(table_name)
 
     def _read_parquet_duckdb_native(
-        self,
-        source_list: str | Iterable[str],
-        table_name: str,
-        **kwargs: Any,
+        self, source_list: str | Iterable[str], table_name: str, **kwargs: Any
     ) -> None:
-        import sqlalchemy_views as sav
-
         if any(
             source.startswith(("http://", "https://", "s3://"))
             for source in source_list
@@ -395,15 +391,12 @@ def _read_parquet_duckdb_native(
                 sa.func.list_value(*source_list), _format_kwargs(kwargs)
             )
         )
-        view = sav.CreateView(sa.table(table_name), source, or_replace=True)
+        view = self._compile_temp_view(table_name, source)
         with self.begin() as con:
-            con.execute(view)
+            con.exec_driver_sql(view)
 
     def _read_parquet_pyarrow_dataset(
-        self,
-        source_list: str | Iterable[str],
-        table_name: str,
-        **kwargs: Any,
+        self, source_list: str | Iterable[str], table_name: str, **kwargs: Any
     ) -> None:
         import pyarrow.dataset as ds
 
@@ -496,8 +489,6 @@ def read_postgres(self, uri, table_name: str | None = None, schema: str = "publi
         ir.Table
             The just-registered table.
         """
-        import sqlalchemy_views as sav
-
         if table_name is None:
             raise ValueError(
                 "`table_name` is required when registering a postgres table"
@@ -506,9 +497,9 @@ def read_postgres(self, uri, table_name: str | None = None, schema: str = "publi
         source = sa.select(sa.literal_column("*")).select_from(
             sa.func.postgres_scan_pushdown(uri, schema, table_name)
         )
-        view = sav.CreateView(sa.table(table_name), source, or_replace=True)
+        view = self._compile_temp_view(table_name, source)
         with self.begin() as con:
-            con.execute(view)
+            con.exec_driver_sql(view)
 
         return self.table(table_name)
 
@@ -540,7 +531,6 @@ def read_sqlite(self, path: str | Path, table_name: str | None = None) -> ir.Tab
             3   0.29  Premium     I     VS2   62.4   58.0    334  4.20  4.23  2.63
             4   0.31     Good     J     SI2   63.3   58.0    335  4.34  4.35  2.75
         """
-        import sqlalchemy_views as sav
 
         if table_name is None:
             raise ValueError("`table_name` is required when registering a sqlite table")
@@ -548,9 +538,9 @@ def read_sqlite(self, path: str | Path, table_name: str | None = None) -> ir.Tab
         source = sa.select(sa.literal_column("*")).select_from(
             sa.func.sqlite_scan(str(path), table_name)
         )
-        view = sav.CreateView(sa.table(table_name), source, or_replace=True)
+        view = self._compile_temp_view(table_name, source)
         with self.begin() as con:
-            con.execute(view)
+            con.exec_driver_sql(view)
 
         return self.table(table_name)
 
diff --git a/ibis/backends/postgres/__init__.py b/ibis/backends/postgres/__init__.py
--- a/ibis/backends/postgres/__init__.py
+++ b/ibis/backends/postgres/__init__.py
@@ -259,4 +259,4 @@ def _metadata(self, query: str) -> Iterable[tuple[str, dt.DataType]]:
     def _get_temp_view_definition(
         self, name: str, definition: sa.sql.compiler.Compiled
     ) -> str:
-        yield f"CREATE OR REPLACE VIEW {name} AS {definition}"
+        yield f"CREATE OR REPLACE TEMPORARY VIEW {name} AS {definition}"
diff --git a/ibis/backends/sqlite/__init__.py b/ibis/backends/sqlite/__init__.py
--- a/ibis/backends/sqlite/__init__.py
+++ b/ibis/backends/sqlite/__init__.py
@@ -270,7 +270,7 @@ def _get_temp_view_definition(
         self, name: str, definition: sa.sql.compiler.Compiled
     ) -> str:
         yield f"DROP VIEW IF EXISTS {name}"
-        yield f"CREATE VIEW {name} AS {definition}"
+        yield f"CREATE TEMPORARY VIEW {name} AS {definition}"
 
     def _get_compiled_statement(self, view: sa.Table, definition: sa.sql.Selectable):
         return super()._get_compiled_statement(


Test patch: