Repository: ibis-project/ibis

Issue:
feat: support using `.sql()` in the Polars backend
### What happened?

see https://github.com/ibis-project/ibis/issues/8029 for setup

then trying to run that SQL on the Polars backend like so:

```python
ibis.set_backend("polars") # <1>
kwargs = polars_kwargs

t = ibis.read_csv("1brc/data/measurements.txt", **kwargs)

res = t.alias("measurements").sql(sql, dialect="duckdb")
res
```

leads to a ComputeError below

### What version of ibis are you using?

main

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

Polars

### Relevant log output

```sh
---------------------------------------------------------------------------
ComputeError                              Traceback (most recent call last)
/Users/cody/repos/ibis/docs/posts/1brc/index.qmd in line 1
----> 344 res = t.alias("measurements").sql(sql, dialect="duckdb")
      345 res

File ~/repos/ibis/ibis/expr/types/relations.py:3221, in Table.sql(self, query, dialect)
   3219     backend = self._find_backend()
   3220     query = backend._transpile_sql(query, dialect=dialect)
-> 3221 op = ops.SQLStringView(child=self, name=next(_ALIASES), query=query)
   3222 return op.to_expr()

File ~/repos/ibis/ibis/common/bases.py:73, in AbstractMeta.__call__(cls, *args, **kwargs)
     54 def __call__(cls, *args, **kwargs):
     55     """Create a new instance of the class.
     56
     57     The subclass may override the `__create__` classmethod to change the
   (...)
     71     The newly created instance of the class. No extra initialization
     72     """
---> 73     return cls.__create__(*args, **kwargs)

File ~/repos/ibis/ibis/common/grounds.py:119, in Annotable.__create__(cls, *args, **kwargs)
    115 @classmethod
    116 def __create__(cls, *args: Any, **kwargs: Any) -> Self:
    117     # construct the instance by passing only validated keyword arguments
    118     kwargs = cls.__signature__.validate(cls, args, kwargs)
--> 119     return super().__create__(**kwargs)

File ~/repos/ibis/ibis/common/grounds.py:208, in Concrete.__init__(self, **kwargs)
    206 for name, field in self.__attributes__.items():
    207     if field.has_default():
--> 208         object.__setattr__(self, name, field.get_default(name, self))

File ~/repos/ibis/ibis/common/annotations.py:174, in Attribute.get_default(self, name, this)
    160 """Get the default value of the field.
    161
    162 Parameters
   (...)
    171 The default value for the field.
    172 """
    173 if callable(self.default):
--> 174     value = self.default(this)
    175 else:
    176     value = self.default

File ~/repos/ibis/ibis/expr/operations/relations.py:594, in SQLStringView.schema(self)
    590 @attribute
    591 def schema(self):
    592     # TODO(kszucs): avoid converting to expression
    593     backend = self.child.to_expr()._find_backend()
--> 594     return backend._get_schema_using_query(self.query)

File ~/repos/ibis/ibis/backends/polars/__init__.py:399, in Backend._get_schema_using_query(self, query)
    397 def _get_schema_using_query(self, query: str) -> sch.Schema:
    398     return schema_from_polars(
--> 399         self._context.execute(f"SELECT * FROM ({query}) LIMIT 0").schema
    400     )

File ~/repos/ibis/venv/lib/python3.11/site-packages/polars/sql/context.py:274, in SQLContext.execute(self, query, eager)
    196 def execute(self, query: str, eager: bool | None = None) -> LazyFrame | DataFrame:
    197     """
    198     Parse the given SQL query and execute it against the registered frame data.
    199
   (...)
    272     └────────┴─────────────┴─────────┘
    273     """
--> 274     res = wrap_ldf(self._ctxt.execute(query))
    275     return res.collect() if (eager or self._eager_execution) else res

ComputeError: Derived tables must have aliases
```


### Code of Conduct

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


Discussion:
In `the-epic-split` branch you now get a nicer not implemented error for polars + `.sql`. This is at least a better error on than on `main`, but doesn't resolve `.sql` support for polars.
~`con.sql` support for polars already exists, so this is a bug.~ I'll open another issue about it.
Ah, well, turns out both `con.sql` and `t.sql` are implemented, and this bug is preventing them from working. 

PR incoming.

Resolving patch:
diff --git a/ibis/backends/polars/__init__.py b/ibis/backends/polars/__init__.py
--- a/ibis/backends/polars/__init__.py
+++ b/ibis/backends/polars/__init__.py
@@ -395,9 +395,7 @@ def compile(
         return translate(node, ctx=ctx)
 
     def _get_schema_using_query(self, query: str) -> sch.Schema:
-        return schema_from_polars(
-            self._context.execute(f"SELECT * FROM ({query}) LIMIT 0").schema
-        )
+        return schema_from_polars(self._context.execute(query).schema)
 
     def execute(
         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
@@ -189,7 +189,7 @@ def test_query_schema(ddl_backend, expr_fn, expected):
 }
 
 
-@pytest.mark.notimpl(["datafusion", "polars", "mssql"])
+@pytest.mark.notimpl(["datafusion", "mssql"])
 @pytest.mark.never(["dask", "pandas"], reason="dask and pandas do not support SQL")
 @pytest.mark.notimpl(
     ["flink"], raises=AttributeError, reason="'Backend' object has no attribute 'sql'"
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
@@ -33,14 +33,7 @@
 @pytest.mark.parametrize(
     "schema",
     [
-        param(
-            None,
-            id="implicit_schema",
-            marks=[
-                pytest.mark.notimpl(["druid"]),
-                pytest.mark.notyet(["polars"], raises=PolarsComputeError),
-            ],
-        ),
+        param(None, id="implicit_schema", marks=[pytest.mark.notimpl(["druid"])]),
         param({"s": "string", "new_col": "double"}, id="explicit_schema"),
     ],
 )
@@ -82,7 +75,6 @@ def test_con_dot_sql(backend, con, schema):
 @dot_sql_notimpl
 @dot_sql_notyet
 @dot_sql_never
-@pytest.mark.notyet(["polars"], raises=PolarsComputeError)
 def test_table_dot_sql(backend, con):
     alltypes = con.table("functional_alltypes")
     t = (
@@ -120,7 +112,6 @@ def test_table_dot_sql(backend, con):
 @dot_sql_notimpl
 @dot_sql_notyet
 @dot_sql_never
-@pytest.mark.notyet(["polars"], raises=PolarsComputeError)
 def test_table_dot_sql_with_join(backend, con):
     alltypes = con.table("functional_alltypes")
     t = (
@@ -168,7 +159,6 @@ def test_table_dot_sql_with_join(backend, con):
 @dot_sql_notimpl
 @dot_sql_notyet
 @dot_sql_never
-@pytest.mark.notyet(["polars"], raises=PolarsComputeError)
 def test_table_dot_sql_repr(con):
     alltypes = con.table("functional_alltypes")
     t = (
@@ -235,12 +225,8 @@ def test_dot_sql_reuse_alias_with_different_types(backend, alltypes, df):
 
 @pytest.mark.parametrize(
     "dialect",
-    [
-        *sorted(_get_backend_names() - _NO_SQLGLOT_DIALECT),
-        *no_sqlglot_dialect,
-    ],
+    [*sorted(_get_backend_names() - _NO_SQLGLOT_DIALECT), *no_sqlglot_dialect],
 )
-@pytest.mark.notyet(["polars"], raises=PolarsComputeError)
 @table_dot_sql_notimpl
 @dot_sql_notimpl
 @dot_sql_notyet
@@ -264,7 +250,6 @@ def test_table_dot_sql_transpile(backend, alltypes, dialect, df):
         *no_sqlglot_dialect,
     ],
 )
-@pytest.mark.notyet(["polars"], raises=PolarsComputeError)
 @pytest.mark.notyet(["druid"], raises=ValueError)
 @pytest.mark.notyet(["snowflake", "bigquery"])
 @pytest.mark.notyet(