Skip to content

Commit e109449

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

2 files changed

Lines changed: 76 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: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Regression test for temp views in relations.
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.
7+
8+
`rel.select()` and `conn.sql()` don't create a view and work fine, only
9+
rel.query() trips the bug.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import duckdb
15+
16+
17+
def test_rel_query_on_readonly_database(tmp_path):
18+
db_path = tmp_path / "readonly.duckdb"
19+
20+
# Step 1: create the database with test data using a writable connection
21+
with duckdb.connect(str(db_path)) as setup_conn:
22+
setup_conn.execute(
23+
"""
24+
CREATE TABLE orders AS
25+
SELECT * FROM (
26+
VALUES (1, 'A', 100), (2, 'B', 250), (3, 'C', 50)
27+
) AS t(order_id, product, quantity)
28+
"""
29+
)
30+
31+
# Step 2: reopen read-only and exercise rel.query()
32+
conn = duckdb.connect(str(db_path), read_only=True)
33+
try:
34+
rel = conn.sql("SELECT * FROM orders")
35+
result = rel.query(
36+
"duckdb_settings()",
37+
"SELECT value FROM duckdb_settings() WHERE name = 'TimeZone'",
38+
).fetchone()
39+
assert result is not None
40+
assert isinstance(result[0], str) # value column is a string
41+
finally:
42+
conn.close()
43+
44+
45+
def test_rel_select_on_readonly_database_still_works(tmp_path):
46+
"""Sanity: rel.select() (which doesn't create a view) must continue to work."""
47+
db_path = tmp_path / "readonly.duckdb"
48+
with duckdb.connect(str(db_path)) as setup_conn:
49+
setup_conn.execute("CREATE TABLE t AS SELECT 1 AS x")
50+
51+
conn = duckdb.connect(str(db_path), read_only=True)
52+
try:
53+
rel = conn.sql("SELECT * FROM t")
54+
result = rel.select(
55+
duckdb.FunctionExpression("current_setting", duckdb.ConstantExpression("TimeZone"))
56+
).fetchone()
57+
assert result is not None
58+
assert isinstance(result[0], str)
59+
finally:
60+
conn.close()
61+
62+
63+
def test_conn_sql_on_readonly_database_still_works(tmp_path):
64+
"""Sanity: conn.sql() (no view created) must continue to work."""
65+
db_path = tmp_path / "readonly.duckdb"
66+
with duckdb.connect(str(db_path)) as setup_conn:
67+
setup_conn.execute("CREATE TABLE t AS SELECT 1 AS x")
68+
69+
conn = duckdb.connect(str(db_path), read_only=True)
70+
try:
71+
result = conn.sql("SELECT value FROM duckdb_settings() WHERE name = 'TimeZone'").fetchone()
72+
assert result is not None
73+
assert isinstance(result[0], str)
74+
finally:
75+
conn.close()

0 commit comments

Comments
 (0)