Skip to content

Commit c8e16c8

Browse files
committed
Make rel->query work with a read only connection
1 parent 811b135 commit c8e16c8

2 files changed

Lines changed: 77 additions & 1 deletion

File tree

src/duckdb_py/pyrelation.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1548,7 +1548,7 @@ static bool IsDescribeStatement(SQLStatement &statement) {
15481548
}
15491549

15501550
unique_ptr<DuckDBPyRelation> DuckDBPyRelation::Query(const string &view_name, const string &sql_query) {
1551-
auto view_relation = CreateView(view_name);
1551+
rel->CreateView(view_name, /*replace=*/true, /*temporary=*/true);
15521552
auto all_dependencies = rel->GetAllDependencies();
15531553

15541554
Parser parser(rel->context->GetContext()->GetParserOptions());
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Regression test for duckdblabs/motherduck#528.
2+
3+
`DuckDBPyRelation.query(view_name, sql)` internally calls CreateView, which
4+
writes to the default catalog. On a read-only attached database the write
5+
fails with InvalidInputException, breaking any user pattern that uses
6+
rel.query() against a read-only database (typical for MotherDuck
7+
read-scaling tokens or any duckdb.connect(path, read_only=True)).
8+
9+
`rel.select()` and `conn.sql()` don't create a view and work fine — only
10+
rel.query() trips the bug.
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import duckdb
16+
17+
18+
def test_rel_query_on_readonly_database(tmp_path):
19+
db_path = tmp_path / "readonly.duckdb"
20+
21+
# Step 1: create the database with test data using a writable connection
22+
with duckdb.connect(str(db_path)) as setup_conn:
23+
setup_conn.execute(
24+
"""
25+
CREATE TABLE orders AS
26+
SELECT * FROM (
27+
VALUES (1, 'A', 100), (2, 'B', 250), (3, 'C', 50)
28+
) AS t(order_id, product, quantity)
29+
"""
30+
)
31+
32+
# Step 2: reopen read-only and exercise rel.query()
33+
conn = duckdb.connect(str(db_path), read_only=True)
34+
try:
35+
rel = conn.sql("SELECT * FROM orders")
36+
result = rel.query(
37+
"duckdb_settings()",
38+
"SELECT value FROM duckdb_settings() WHERE name = 'TimeZone'",
39+
).fetchone()
40+
assert result is not None
41+
assert isinstance(result[0], str) # value column is a string
42+
finally:
43+
conn.close()
44+
45+
46+
def test_rel_select_on_readonly_database_still_works(tmp_path):
47+
"""Sanity: rel.select() (which doesn't create a view) must continue to work."""
48+
db_path = tmp_path / "readonly.duckdb"
49+
with duckdb.connect(str(db_path)) as setup_conn:
50+
setup_conn.execute("CREATE TABLE t AS SELECT 1 AS x")
51+
52+
conn = duckdb.connect(str(db_path), read_only=True)
53+
try:
54+
rel = conn.sql("SELECT * FROM t")
55+
result = rel.select(
56+
duckdb.FunctionExpression("current_setting", duckdb.ConstantExpression("TimeZone"))
57+
).fetchone()
58+
assert result is not None
59+
assert isinstance(result[0], str)
60+
finally:
61+
conn.close()
62+
63+
64+
def test_conn_sql_on_readonly_database_still_works(tmp_path):
65+
"""Sanity: conn.sql() (no view created) must continue to work."""
66+
db_path = tmp_path / "readonly.duckdb"
67+
with duckdb.connect(str(db_path)) as setup_conn:
68+
setup_conn.execute("CREATE TABLE t AS SELECT 1 AS x")
69+
70+
conn = duckdb.connect(str(db_path), read_only=True)
71+
try:
72+
result = conn.sql("SELECT value FROM duckdb_settings() WHERE name = 'TimeZone'").fetchone()
73+
assert result is not None
74+
assert isinstance(result[0], str)
75+
finally:
76+
conn.close()

0 commit comments

Comments
 (0)