From 9fe40567648555b9dcada83b8f40b5a317b32ed8 Mon Sep 17 00:00:00 2001 From: Evert Lammerts Date: Wed, 11 Feb 2026 12:56:18 +0100 Subject: [PATCH] Treat empty params the same as None for .sql --- src/duckdb_py/pyconnection.cpp | 5 ++- tests/fast/api/test_sql_params_performance.py | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 tests/fast/api/test_sql_params_performance.py diff --git a/src/duckdb_py/pyconnection.cpp b/src/duckdb_py/pyconnection.cpp index c786421f..150fbdc1 100644 --- a/src/duckdb_py/pyconnection.cpp +++ b/src/duckdb_py/pyconnection.cpp @@ -1600,8 +1600,9 @@ unique_ptr DuckDBPyConnection::RunQuery(const py::object &quer // Attempt to create a Relation for lazy execution if possible shared_ptr relation; - if (py::none().is(params)) { - // FIXME: currently we can't create relations with prepared parameters + bool has_params = !py::none().is(params) && py::len(params) > 0; + if (!has_params) { + // No params (or empty params) — use lazy QueryRelation path { D_ASSERT(py::gil_check()); py::gil_scoped_release gil; diff --git a/tests/fast/api/test_sql_params_performance.py b/tests/fast/api/test_sql_params_performance.py new file mode 100644 index 00000000..b2654e8c --- /dev/null +++ b/tests/fast/api/test_sql_params_performance.py @@ -0,0 +1,40 @@ +import time + + +class TestSqlEmptyParams: + """Empty params should use lazy QueryRelation path (same as params=None).""" + + def test_empty_list_returns_same_result(self, duckdb_cursor): + """sql(params=[]) returns same data as sql(params=None).""" + duckdb_cursor.execute("CREATE TABLE t AS SELECT i FROM range(10) t(i)") + expected = duckdb_cursor.sql("SELECT * FROM t").fetchall() + result = duckdb_cursor.sql("SELECT * FROM t", params=[]).fetchall() + assert result == expected + + def test_empty_dict_returns_same_result(self, duckdb_cursor): + """sql(params={}) returns same data as sql(params=None).""" + duckdb_cursor.execute("CREATE TABLE t AS SELECT i FROM range(10) t(i)") + expected = duckdb_cursor.sql("SELECT * FROM t").fetchall() + result = duckdb_cursor.sql("SELECT * FROM t", params={}).fetchall() + assert result == expected + + def test_empty_tuple_returns_same_result(self, duckdb_cursor): + """sql(params=()) returns same data as sql(params=None).""" + duckdb_cursor.execute("CREATE TABLE t AS SELECT i FROM range(10) t(i)") + expected = duckdb_cursor.sql("SELECT * FROM t").fetchall() + result = duckdb_cursor.sql("SELECT * FROM t", params=()).fetchall() + assert result == expected + + def test_empty_params_is_chainable(self, duckdb_cursor): + """Empty params produces a real relation that supports chaining.""" + duckdb_cursor.execute("CREATE TABLE t AS SELECT i FROM range(10) t(i)") + result = duckdb_cursor.sql("SELECT * FROM t", params=[]).filter("i < 3").order("i").fetchall() + assert result == [(0,), (1,), (2,)] + + def test_empty_params_explain_is_fast(self, duckdb_cursor): + """Empty params explain should not trigger expensive ToString.""" + duckdb_cursor.execute("CREATE TABLE t AS SELECT i FROM range(100000) t(i)") + t0 = time.perf_counter() + duckdb_cursor.sql("SELECT * FROM t", params=[]).explain() + elapsed = time.perf_counter() - t0 + assert elapsed < 5.0, f"explain() took {elapsed:.2f}s, expected < 5s"