Skip to content

Commit b6efa5c

Browse files
Treat empty params the same as None for .sql (#310)
Related to #238 This makes sure that passing in empty params results in a `QueryRelation` being created instead of a `MaterializedRelation`. See [this comment](#238 (comment)) for more context.
2 parents d37ef1d + 9fe4056 commit b6efa5c

2 files changed

Lines changed: 43 additions & 2 deletions

File tree

src/duckdb_py/pyconnection.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,8 +1600,9 @@ unique_ptr<DuckDBPyRelation> DuckDBPyConnection::RunQuery(const py::object &quer
16001600

16011601
// Attempt to create a Relation for lazy execution if possible
16021602
shared_ptr<Relation> relation;
1603-
if (py::none().is(params)) {
1604-
// FIXME: currently we can't create relations with prepared parameters
1603+
bool has_params = !py::none().is(params) && py::len(params) > 0;
1604+
if (!has_params) {
1605+
// No params (or empty params) — use lazy QueryRelation path
16051606
{
16061607
D_ASSERT(py::gil_check());
16071608
py::gil_scoped_release gil;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import time
2+
3+
4+
class TestSqlEmptyParams:
5+
"""Empty params should use lazy QueryRelation path (same as params=None)."""
6+
7+
def test_empty_list_returns_same_result(self, duckdb_cursor):
8+
"""sql(params=[]) returns same data as sql(params=None)."""
9+
duckdb_cursor.execute("CREATE TABLE t AS SELECT i FROM range(10) t(i)")
10+
expected = duckdb_cursor.sql("SELECT * FROM t").fetchall()
11+
result = duckdb_cursor.sql("SELECT * FROM t", params=[]).fetchall()
12+
assert result == expected
13+
14+
def test_empty_dict_returns_same_result(self, duckdb_cursor):
15+
"""sql(params={}) returns same data as sql(params=None)."""
16+
duckdb_cursor.execute("CREATE TABLE t AS SELECT i FROM range(10) t(i)")
17+
expected = duckdb_cursor.sql("SELECT * FROM t").fetchall()
18+
result = duckdb_cursor.sql("SELECT * FROM t", params={}).fetchall()
19+
assert result == expected
20+
21+
def test_empty_tuple_returns_same_result(self, duckdb_cursor):
22+
"""sql(params=()) returns same data as sql(params=None)."""
23+
duckdb_cursor.execute("CREATE TABLE t AS SELECT i FROM range(10) t(i)")
24+
expected = duckdb_cursor.sql("SELECT * FROM t").fetchall()
25+
result = duckdb_cursor.sql("SELECT * FROM t", params=()).fetchall()
26+
assert result == expected
27+
28+
def test_empty_params_is_chainable(self, duckdb_cursor):
29+
"""Empty params produces a real relation that supports chaining."""
30+
duckdb_cursor.execute("CREATE TABLE t AS SELECT i FROM range(10) t(i)")
31+
result = duckdb_cursor.sql("SELECT * FROM t", params=[]).filter("i < 3").order("i").fetchall()
32+
assert result == [(0,), (1,), (2,)]
33+
34+
def test_empty_params_explain_is_fast(self, duckdb_cursor):
35+
"""Empty params explain should not trigger expensive ToString."""
36+
duckdb_cursor.execute("CREATE TABLE t AS SELECT i FROM range(100000) t(i)")
37+
t0 = time.perf_counter()
38+
duckdb_cursor.sql("SELECT * FROM t", params=[]).explain()
39+
elapsed = time.perf_counter() - t0
40+
assert elapsed < 5.0, f"explain() took {elapsed:.2f}s, expected < 5s"

0 commit comments

Comments
 (0)