|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +import sys |
5 | 6 | from copy import deepcopy |
6 | 7 | from typing import TYPE_CHECKING |
| 8 | +from unittest import mock |
7 | 9 |
|
8 | 10 | import pytest |
9 | 11 |
|
@@ -228,8 +230,6 @@ def test_duckdb_engine_sql_output_formats( |
228 | 230 | duckdb_connection: duckdb.DuckDBPyConnection, |
229 | 231 | ) -> None: |
230 | 232 | """Test DuckDBEngine execute with different SQL output formats.""" |
231 | | - from unittest import mock |
232 | | - |
233 | 233 | import pandas as pd |
234 | 234 | import polars as pl |
235 | 235 |
|
@@ -290,3 +290,58 @@ def test_duckdb_engine_sql_output_formats( |
290 | 290 | result = engine.execute("SELECT * FROM test ORDER BY id") |
291 | 291 | assert isinstance(result, (pd.DataFrame, pl.DataFrame)) |
292 | 292 | assert len(result) == 4 |
| 293 | + |
| 294 | + |
| 295 | +@pytest.mark.skipif( |
| 296 | + not HAS_DUCKDB or not HAS_POLARS, |
| 297 | + reason="DuckDB and Polars not installed", |
| 298 | +) |
| 299 | +@pytest.mark.parametrize( |
| 300 | + ("sql_output_format", "expected_type_name"), |
| 301 | + [ |
| 302 | + ("polars", "DataFrame"), |
| 303 | + ("lazy-polars", "LazyFrame"), |
| 304 | + ("auto", "DataFrame"), |
| 305 | + ], |
| 306 | +) |
| 307 | +def test_duckdb_engine_polars_no_pyarrow( |
| 308 | + duckdb_connection: duckdb.DuckDBPyConnection, |
| 309 | + sql_output_format: str, |
| 310 | + expected_type_name: str, |
| 311 | +) -> None: |
| 312 | + """Polars conversion should not require pyarrow. |
| 313 | +
|
| 314 | + Uses the Arrow PyCapsule interface (`pl.DataFrame(relation)`) rather than |
| 315 | + `relation.pl()` which historically required pyarrow. Covers every output |
| 316 | + format that routes through `to_polars()` (polars, lazy-polars, and auto |
| 317 | + when polars is installed). |
| 318 | + """ |
| 319 | + import polars as pl |
| 320 | + |
| 321 | + # Block `pyarrow` and any already-imported `pyarrow.*` submodules so that |
| 322 | + # fresh imports raise ModuleNotFoundError. |
| 323 | + blocked_pyarrow = { |
| 324 | + name: None |
| 325 | + for name in list(sys.modules) |
| 326 | + if name == "pyarrow" or name.startswith("pyarrow.") |
| 327 | + } |
| 328 | + blocked_pyarrow["pyarrow"] = None |
| 329 | + |
| 330 | + with ( |
| 331 | + mock.patch.dict(sys.modules, blocked_pyarrow), |
| 332 | + mock.patch.object( |
| 333 | + DuckDBEngine, "sql_output_format", return_value=sql_output_format |
| 334 | + ), |
| 335 | + ): |
| 336 | + engine = DuckDBEngine( |
| 337 | + duckdb_connection, |
| 338 | + engine_name=VariableName("test_duckdb"), |
| 339 | + ) |
| 340 | + result = engine.execute("SELECT * FROM test ORDER BY id") |
| 341 | + expected_type = getattr(pl, expected_type_name) |
| 342 | + assert isinstance(result, expected_type) |
| 343 | + # Collect lazy frames so we exercise the full polars conversion path. |
| 344 | + materialized = ( |
| 345 | + result.collect() if expected_type_name == "LazyFrame" else result |
| 346 | + ) |
| 347 | + assert len(materialized) == 4 |
0 commit comments