From e109449a108f87959648077e7e4269c5b48c7727 Mon Sep 17 00:00:00 2001 From: Evert Lammerts Date: Tue, 26 May 2026 16:27:30 +0200 Subject: [PATCH] Make rel->query work with a read only connection --- src/duckdb_py/pyrelation.cpp | 2 +- tests/fast/api/test_528_rel_query_readonly.py | 75 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 tests/fast/api/test_528_rel_query_readonly.py diff --git a/src/duckdb_py/pyrelation.cpp b/src/duckdb_py/pyrelation.cpp index 35e33786..51a44f87 100644 --- a/src/duckdb_py/pyrelation.cpp +++ b/src/duckdb_py/pyrelation.cpp @@ -1548,7 +1548,7 @@ static bool IsDescribeStatement(SQLStatement &statement) { } unique_ptr DuckDBPyRelation::Query(const string &view_name, const string &sql_query) { - auto view_relation = CreateView(view_name); + rel->CreateView(view_name, /*replace=*/true, /*temporary=*/true); auto all_dependencies = rel->GetAllDependencies(); Parser parser(rel->context->GetContext()->GetParserOptions()); diff --git a/tests/fast/api/test_528_rel_query_readonly.py b/tests/fast/api/test_528_rel_query_readonly.py new file mode 100644 index 00000000..1054ce66 --- /dev/null +++ b/tests/fast/api/test_528_rel_query_readonly.py @@ -0,0 +1,75 @@ +"""Regression test for temp views in relations. + +`DuckDBPyRelation.query(view_name, sql)` internally calls CreateView, which +writes to the default catalog. On a read-only attached database the write +fails with InvalidInputException, breaking any user pattern that uses +rel.query() against a read-only database. + +`rel.select()` and `conn.sql()` don't create a view and work fine, only +rel.query() trips the bug. +""" + +from __future__ import annotations + +import duckdb + + +def test_rel_query_on_readonly_database(tmp_path): + db_path = tmp_path / "readonly.duckdb" + + # Step 1: create the database with test data using a writable connection + with duckdb.connect(str(db_path)) as setup_conn: + setup_conn.execute( + """ + CREATE TABLE orders AS + SELECT * FROM ( + VALUES (1, 'A', 100), (2, 'B', 250), (3, 'C', 50) + ) AS t(order_id, product, quantity) + """ + ) + + # Step 2: reopen read-only and exercise rel.query() + conn = duckdb.connect(str(db_path), read_only=True) + try: + rel = conn.sql("SELECT * FROM orders") + result = rel.query( + "duckdb_settings()", + "SELECT value FROM duckdb_settings() WHERE name = 'TimeZone'", + ).fetchone() + assert result is not None + assert isinstance(result[0], str) # value column is a string + finally: + conn.close() + + +def test_rel_select_on_readonly_database_still_works(tmp_path): + """Sanity: rel.select() (which doesn't create a view) must continue to work.""" + db_path = tmp_path / "readonly.duckdb" + with duckdb.connect(str(db_path)) as setup_conn: + setup_conn.execute("CREATE TABLE t AS SELECT 1 AS x") + + conn = duckdb.connect(str(db_path), read_only=True) + try: + rel = conn.sql("SELECT * FROM t") + result = rel.select( + duckdb.FunctionExpression("current_setting", duckdb.ConstantExpression("TimeZone")) + ).fetchone() + assert result is not None + assert isinstance(result[0], str) + finally: + conn.close() + + +def test_conn_sql_on_readonly_database_still_works(tmp_path): + """Sanity: conn.sql() (no view created) must continue to work.""" + db_path = tmp_path / "readonly.duckdb" + with duckdb.connect(str(db_path)) as setup_conn: + setup_conn.execute("CREATE TABLE t AS SELECT 1 AS x") + + conn = duckdb.connect(str(db_path), read_only=True) + try: + result = conn.sql("SELECT value FROM duckdb_settings() WHERE name = 'TimeZone'").fetchone() + assert result is not None + assert isinstance(result[0], str) + finally: + conn.close()