Description
fetch_rel_time_zone() in narwhals/_duckdb/utils.py:207 fails when the DuckDB relation is connected to MotherDuck via a read-only token. The error occurs because rel.query() internally creates a temporary view in the relation's database context, which is a write operation that fails on a read-only database.
This affects any Narwhals operation that touches the schema of a DuckDB relation containing a TIMESTAMPTZ column — including collect_schema(), filtering, selecting, joining, etc.
Error
Invalid Input Error: Failed to create view 'duckdb_settings()':
Cannot execute statement of type "RELATION" on database "my_db" which is attached in read-only mode!
Root Cause
The current implementation at utils.py:207-212:
def fetch_rel_time_zone(rel: duckdb.DuckDBPyRelation) -> str:
result = rel.query(
"duckdb_settings()", "select value from duckdb_settings() where name = 'TimeZone'"
).fetchone()
assert result is not None
return result[0]
rel.query(view_name, sql) works by:
- Creating a temporary view (named
"duckdb_settings()") inside the relation's database — this is a write operation
- Running the SQL query
- Dropping the view
When the relation belongs to a read-only MotherDuck database, step 1 fails.
Note: calling conn.sql("SELECT value FROM duckdb_settings() WHERE name = 'TimeZone'") directly on the connection works fine — the issue is specifically rel.query().
Call Chain
df.collect_schema() # dataframe.py:387
→ self.schema # dataframe.py:235
→ DeferredTimeZone(self.native) # dataframe.py:241
→ native_to_narwhals_dtype(...) # dataframe.py:243
→ if duckdb_dtype_id == "timestamp with time zone" # utils.py:195
→ deferred_time_zone.time_zone # utils.py:196
→ fetch_rel_time_zone(rel) # utils.py:146
→ rel.query("duckdb_settings()", ...) # utils.py:208 ← FAILS
Reproduction
Environment: macOS (darwin), Python 3.14.0, DuckDB 1.5.2, Narwhals 2.20.0
import duckdb
import narwhals as nw
# Connect with a read-only MotherDuck token
conn = duckdb.connect("md:?motherduck_token=<read-only-token>")
# Test 1: Raw duckdb_settings() via conn.sql() — PASSES
conn.sql("SELECT value FROM duckdb_settings() WHERE name = 'TimeZone'").fetchone()
# OK: TimeZone = America/Los_Angeles
# Test 2: Narwhals on a TIMESTAMPTZ column — FAILS
rel = conn.sql("SELECT now() AS ts")
df = nw.from_native(rel)
df.collect_schema()
# FAILED: Invalid Input Error: Failed to create view 'duckdb_settings()':
# Cannot execute statement of type "RELATION" on database "my_db" which is attached in read-only mode!
# Test 3: Narwhals on non-timestamp columns — PASSES (control)
rel = conn.sql("SELECT 1 AS x, 'hello' AS y")
df = nw.from_native(rel)
df.collect_schema()
# OK: schema = Schema({'x': Int32, 'y': String})
# Test 4a: rel.query() directly — FAILS (isolates the issue)
rel = conn.sql("SELECT now() AS ts")
rel.query("duckdb_settings()", "SELECT value FROM duckdb_settings() WHERE name = 'TimeZone'").fetchone()
# FAILED: same error
# Test 4b: conn.sql() directly — PASSES
conn.sql("SELECT value FROM duckdb_settings() WHERE name = 'TimeZone'").fetchone()
# OK: TimeZone = America/Los_Angeles
Tests 4a vs 4b confirm the issue is rel.query() (creates a temp view in the read-only DB) vs conn.sql() (no temp view).
Suggested Approaches
Approach A: rel.limit(1).select(current_setting(...))
Replace rel.query() with the relational API, which does not create a temp view:
from duckdb import ConstantExpression, FunctionExpression
def fetch_rel_time_zone(rel: duckdb.DuckDBPyRelation) -> str:
result = rel.limit(1).select(
FunctionExpression("current_setting", ConstantExpression("TimeZone"))
).fetchone()
assert result is not None
return result[0]
Pros: No API change. Fixes it for all users. Stays on the same connection as the relation (respects custom timezone). No temp view creation.
Cons: Still routes through the user's relation (mitigated by limit(1)).
Approach B: Accept optional conn parameter
Thread an optional connection through from_native() → DuckDBLazyFrame → DeferredTimeZone → fetch_rel_time_zone():
def fetch_rel_time_zone(
rel: duckdb.DuckDBPyRelation,
conn: duckdb.DuckDBPyConnection | None = None,
) -> str:
if conn is not None:
result = conn.sql(
"SELECT value FROM duckdb_settings() WHERE name = 'TimeZone'"
).fetchone()
else:
# fallback to current behavior
result = rel.query(
"duckdb_settings()", "select value from duckdb_settings() where name = 'TimeZone'"
).fetchone()
assert result is not None
return result[0]
Pros: Clean separation — uses conn.sql() when available, no temp view. Per duckdb/duckdb#17033, the recommended best practice in DuckDB is to pass conn alongside DuckDBPyRelation when working with relations in functions, since there is no way to retrieve the connection from a relation.
Cons: Requires API change (from_native(rel, conn=conn)). Only helps users who know to pass conn. Risk of timezone desync if user passes a different connection than the one that created rel.
Note on DeferredTimeZone design
The existing docstring on DeferredTimeZone explains why the timezone can't be cached at from_native() time — users can change the timezone mid-session via rel.query("set timezone = '...'"). Any fix needs to preserve this lazy-fetch behavior.
Description
fetch_rel_time_zone()innarwhals/_duckdb/utils.py:207fails when the DuckDB relation is connected to MotherDuck via a read-only token. The error occurs becauserel.query()internally creates a temporary view in the relation's database context, which is a write operation that fails on a read-only database.This affects any Narwhals operation that touches the schema of a DuckDB relation containing a
TIMESTAMPTZcolumn — includingcollect_schema(), filtering, selecting, joining, etc.Error
Root Cause
The current implementation at
utils.py:207-212:rel.query(view_name, sql)works by:"duckdb_settings()") inside the relation's database — this is a write operationWhen the relation belongs to a read-only MotherDuck database, step 1 fails.
Note: calling
conn.sql("SELECT value FROM duckdb_settings() WHERE name = 'TimeZone'")directly on the connection works fine — the issue is specificallyrel.query().Call Chain
Reproduction
Environment: macOS (darwin), Python 3.14.0, DuckDB 1.5.2, Narwhals 2.20.0
Tests 4a vs 4b confirm the issue is
rel.query()(creates a temp view in the read-only DB) vsconn.sql()(no temp view).Suggested Approaches
Approach A:
rel.limit(1).select(current_setting(...))Replace
rel.query()with the relational API, which does not create a temp view:Pros: No API change. Fixes it for all users. Stays on the same connection as the relation (respects custom timezone). No temp view creation.
Cons: Still routes through the user's relation (mitigated by
limit(1)).Approach B: Accept optional
connparameterThread an optional connection through
from_native()→DuckDBLazyFrame→DeferredTimeZone→fetch_rel_time_zone():Pros: Clean separation — uses
conn.sql()when available, no temp view. Per duckdb/duckdb#17033, the recommended best practice in DuckDB is to pass conn alongside DuckDBPyRelation when working with relations in functions, since there is no way to retrieve the connection from a relation.Cons: Requires API change (
from_native(rel, conn=conn)). Only helps users who know to passconn. Risk of timezone desync if user passes a different connection than the one that createdrel.Note on
DeferredTimeZonedesignThe existing docstring on
DeferredTimeZoneexplains why the timezone can't be cached atfrom_native()time — users can change the timezone mid-session viarel.query("set timezone = '...'"). Any fix needs to preserve this lazy-fetch behavior.