Repository: ibis-project/ibis

Issue:
docs: Explain what "releasing a cursor" means for `raw_sql` 
### Is your feature request related to a problem?

I am reading the docs and later the code to find this explanation: https://github.com/ibis-project/ibis/blob/87a099c5dd83bd95c44c9c7bfbb9a0d2f4e2837d/ibis/backends/base/sql/__init__.py#L132

### Describe the solution you'd like

I would expect to see hints on what releasing a cursor means and how to accomplish it, e.g. how to convert the query results to a Pandas DataFrame.

### What version of ibis are you running?

6.1.0

### What backend(s) are you using, if any?

PostgreSQL

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:
@fleimgruber 👋🏻 Hi! Thanks for opening the issue!

> I would expect to see hints on what releasing a cursor means and how to accomplish it

Fair enough!

> how to convert the query results to a Pandas DataFrame.

Enumerating all the ways this can be done from the result of `raw_sql` in our documentation isn't really in scope for us. After all, one of the things ibis does for you is worry about the details of converting query results to various in-memory formats including pandas DataFrames.

Have you tried using `con.sql` instead of `raw_sql`, or `table.sql` (for running SQL against arbitrary ibis expressions)?

`raw_sql` should be used as a last resort or for things that don't return results like DDL.

`con.sql` and `table.sql` will handle the in-memory-results conversion for you, perhaps obviating your need to use `raw_sql`.

Resolving patch:
diff --git a/ibis/backends/base/sql/__init__.py b/ibis/backends/base/sql/__init__.py
--- a/ibis/backends/base/sql/__init__.py
+++ b/ibis/backends/base/sql/__init__.py
@@ -127,14 +127,46 @@ def _get_schema_using_query(self, query):
         raise NotImplementedError(f"Backend {self.name} does not support .sql()")
 
     def raw_sql(self, query: str):
-        """Execute a query string.
+        """Execute a query string and return the cursor used for execution.
 
-        !!! warning "The returned cursor object must be **manually** released if results are returned."
+        !!! tip "Consider using [`.sql`][ibis.backends.base.sql.BaseSQLBackend.sql] instead"
+
+            If your query is a SELECT statement, you should use the
+            [`.sql`][ibis.backends.base.sql.BaseSQLBackend.sql] method to avoid
+            having to release the cursor returned from this method manually.
+
+            ??? warning "The returned cursor object must be **manually released** if you use `raw_sql`."
+
+                To release a cursor, call the `close` method on the returned cursor object.
+
+                You can close the cursor by explicitly calling its `close` method:
+
+                ```python
+                cursor = con.raw_sql("SELECT ...")
+                cursor.close()
+                ```
+
+                Or you can use a context manager:
+
+                ```python
+                with con.raw_sql("SELECT ...") as cursor:
+                    ...
+                ```
 
         Parameters
         ----------
         query
             DDL or DML statement
+
+        Examples
+        --------
+        >>> con = ibis.connect("duckdb://")
+        >>> with con.raw_sql("SELECT 1") as cursor:
+        ...     result = cursor.fetchall()
+        >>> result
+        [(1,)]
+        >>> cursor.closed
+        True
         """
         return self.con.execute(query)
 
diff --git a/ibis/backends/base/sql/alchemy/__init__.py b/ibis/backends/base/sql/alchemy/__init__.py
--- a/ibis/backends/base/sql/alchemy/__init__.py
+++ b/ibis/backends/base/sql/alchemy/__init__.py
@@ -578,16 +578,7 @@ def _handle_failed_column_type_inference(
                 )
         return table
 
-    def raw_sql(self, query) -> None:
-        """Execute a query string.
-
-        !!! warning "The returned cursor object must be **manually** released."
-
-        Parameters
-        ----------
-        query
-            DDL or DML statement
-        """
+    def raw_sql(self, query):
         return self.con.connect().execute(
             sa.text(query) if isinstance(query, str) else query
         )


Test patch: