Skip to content

bug: fetch_rel_time_zone fails with read-only MotherDuck connection #3567

@spark-dataduck

Description

@spark-dataduck

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:

  1. Creating a temporary view (named "duckdb_settings()") inside the relation's database — this is a write operation
  2. Running the SQL query
  3. 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()DuckDBLazyFrameDeferredTimeZonefetch_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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions